├── .coveragerc ├── .flake8 ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_ethereum_events ├── __init__.py ├── admin.py ├── apps.py ├── chainevents.py ├── decoder.py ├── event_listener.py ├── exceptions.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── reset_block_daemon.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180531_0806.py │ ├── 0003_auto_20180531_0836.py │ ├── 0004_failedeventlog.py │ ├── 0005_auto_20180713_1130.py │ └── __init__.py ├── models.py ├── settings │ ├── __init__.py │ ├── dev.py │ └── test.py ├── signals.py ├── tasks.py ├── tests │ ├── __init__.py │ ├── contracts │ │ ├── __init__.py │ │ ├── bank.py │ │ └── claim.py │ ├── deposit_event_log.json │ ├── test_decoder.py │ └── test_event_listener.py ├── utils.py └── web3_service.py ├── example ├── README.md ├── __init__.py ├── admin.py ├── apps.py ├── contracts │ ├── README.md │ ├── contracts │ │ ├── Echo.sol │ │ └── Migrations.sol │ ├── migrations │ │ ├── 1_initial_migration.js │ │ └── 2_deploy_echo.js │ ├── package-lock.json │ ├── package.json │ ├── test │ │ └── echo.js │ └── truffle-config.js ├── manage.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── clean_state.py │ │ ├── register_events.py │ │ ├── run_listener.py │ │ └── send_echo.py ├── migrations │ └── __init__.py ├── models.py ├── settings.py ├── tests.py ├── urls.py └── views.py ├── runtests.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = django_ethereum_events/* 4 | omit = 5 | */migrations/*.py 6 | */tests/*.py 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | .tox, 5 | __pycache__, 6 | **/migrations/*, 7 | **/tests/*, 8 | **/settings/*, 9 | *~ 10 | max-line-length = 120 11 | 12 | application-import-names = 13 | django_ethereum_events 14 | 15 | ignore = 16 | # D100: Missing docstring in public module 17 | D100, 18 | # D101: Missing docstring in public class 19 | D101, 20 | # D102: Missing docstring in public method 21 | D102, 22 | # D103: Missing docstring in public function 23 | D103, 24 | # D104: Missing docstring in public package 25 | D104, 26 | # D105 Missing docstring in magic method 27 | D105, 28 | # D106 Missing docstring in public nested class 29 | D106, 30 | # D107: Missing docstring in __init__ 31 | D107, 32 | # D401: First line should be in imperative mood 33 | D401, 34 | # D413 Missing blank line after last section 35 | D413, 36 | # Q000: Remove bad quotes 37 | Q000 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.py[cd] 3 | __pycache__ 4 | 5 | # Testing tools 6 | /.tox 7 | /.coverage* 8 | !/.coveragerc 9 | 10 | # Packaging 11 | /dist 12 | /build 13 | *.egg-info 14 | 15 | # Utility 16 | *.sqlite3 17 | /makemigrations.py 18 | /.idea 19 | /.vscode 20 | /.settings 21 | 22 | # Temporary 23 | *~ 24 | *.tmp 25 | *.bak 26 | *.swp 27 | 28 | .project 29 | .pydevproject 30 | .coverage 31 | .coveragerc 32 | 33 | # Example 34 | example/contracts/chain 35 | example/contracts/node_modules 36 | example/contracts/build 37 | example/db.sqlite3 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: python 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | addons: 7 | apt: 8 | packages: 9 | - libgmp-dev 10 | - libffi-dev 11 | env: 12 | - DJANGO=1.11 WEBTHREE=5.5 13 | - DJANGO=2.0 WEBTHREE=5.5 14 | - DJANGO=2.1 WEBTHREE=5.5 15 | - DJANGO=2.2 WEBTHREE=5.5 16 | - DJANGO=3.0 WEBTHREE=5.5 17 | - DJANGO=3.1 WEBTHREE=5.5 18 | matrix: 19 | include: 20 | - python: "3.6" 21 | env: TOXENV=flake8 22 | install: 23 | - pip install tox-travis codecov 24 | script: 25 | - tox 26 | after_success: 27 | - codecov 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To start development you should begin by cloning the repo. 4 | 5 | ```bash 6 | $ git clone https://github.com/artemistomaras/django-ethereum-events.git 7 | ``` 8 | 9 | 10 | # Pull Requests 11 | 12 | In general, pull requests are welcome. Please try to adhere to the following. 13 | 14 | - code should conform to PEP8 and as well as the linting done by flake8 15 | - include tests. 16 | - include any relevant documentation updates. 17 | 18 | It's a good idea to make pull requests early on. A pull request represents the 19 | start of a discussion, and doesn't necessarily need to be the final, finished 20 | submission. 21 | 22 | GitHub's documentation for working on pull requests is [available here](https://help.github.com/articles/creating-a-pull-request/). 23 | 24 | Always run the tests before submitting pull requests, and ideally run `tox` in 25 | order to check that your modifications don't break anything. 26 | 27 | Once you've made a pull request take a look at the travis build status in the 28 | GitHub interface and make sure the tests are runnning as you'd expect. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 artemistomaras 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ###################### 2 | django-ethereum-events 3 | ###################### 4 | 5 | Ethereum Contract Event Log monitoring in Django 6 | 7 | .. image:: https://app.travis-ci.com/artemistomaras/django-ethereum-events.svg?branch=master 8 | :target: https://app.travis-ci.com/github/artemistomaras/django-ethereum-events 9 | 10 | .. image:: https://img.shields.io/pypi/v/django-ethereum-events.svg 11 | :target: https://pypi.python.org/pypi/django-ethereum-events 12 | 13 | 14 | ******** 15 | Overview 16 | ******** 17 | 18 | This package provides an easy way to monitor an ethereum blockchain for transactions that invoke `Contract Events`_ that are of particular interest. 19 | 20 | The main concept was inspired by the following project: 21 | 22 | - https://github.com/gnosis/django-eth-events 23 | 24 | Package versions **0.2.x+** support **web3 v4**. 25 | 26 | If you want python 2.7 compatibility and/or **web3 v3** support, use version **0.1.x** of this package. 27 | 28 | 29 | .. _`Contract Events`: http://solidity.readthedocs.io/en/develop/contracts.html#events 30 | 31 | ************ 32 | Installation 33 | ************ 34 | 35 | 1. Install using pip: 36 | 37 | :: 38 | 39 | pip install django-ethereum-events 40 | 41 | 42 | 2. Make sure to include ``'django_ethereum_events'`` in your ``INSTALLED_APPS`` 43 | 44 | .. code-block:: python 45 | 46 | INSTALLED_APPS += ('django_ethereum_events') 47 | 48 | if you are using the **admin backend**, also include ``solo`` in your ``INSTALLED_APPS``. 49 | 50 | 3. Make necessary migrations 51 | 52 | .. code-block:: python 53 | 54 | python manage.py migrate django_ethereum_events 55 | 56 | 57 | ***** 58 | Usage 59 | ***** 60 | 61 | 1. In your ``settings`` file, specify the following settings 62 | 63 | .. code-block:: python 64 | 65 | ETHEREUM_NODE_URI = 'http://localhost:8545' 66 | 67 | 68 | 2. Create a new MonitoredEvent 69 | 70 | .. code-block:: python 71 | 72 | contract_abi = """ 73 | The whole contract abi goes here 74 | """ 75 | 76 | event = "MyEvent" # the emitted event name 77 | event_receiver = "myapp.event_receivers.CustomEventReceiver" 78 | contract_address = "0x10f683d9acc908cA6b7A34726271229B846b0292" # the address of the contract emitting the event 79 | 80 | MonitoredEvent.object.register_event( 81 | event_name=event, 82 | contract_address=contract_address, 83 | contract_abi=contract_abi, 84 | event_receiver=event_receiver 85 | ) 86 | 87 | 3. Create an appropriate event receiver 88 | 89 | .. code-block:: python 90 | 91 | from django_ethereum_events.chainevents import AbstractEventReceiver 92 | 93 | class CustomEventReceiver(AbsractEventReceiver): 94 | def save(self, decoded_event): 95 | # custom logic goes here 96 | 97 | The ``decoded_event`` parameter is the decoded log as provided from `web3.utils.events.get_event_data`_ method. 98 | 99 | .. _`web3.utils.events.get_event_data`: https://github.com/ethereum/web3.py/blob/v5.5.0/web3/_utils/events.py#L198 100 | 101 | 4. To start monitoring the blockchain, either run the celery task ``django_ethereum_events.tasks.event_listener`` or better, use ``celerybeat`` to run it as a periodical task 102 | 103 | .. code-block:: python 104 | 105 | from celery.beat import crontab 106 | 107 | CELERYBEAT_SCHEDULE = { 108 | 'ethereum_events': { 109 | 'task': 'django_ethereum_events.tasks.event_listener', 110 | 'schedule': crontab(minute='*/1') # run every minute 111 | } 112 | } 113 | 114 | You can also set the optional ``ETHEREUM_LOGS_BATCH_SIZE`` setting which limits the maximum amount of the blocks that can be read at a time from the celery task. 115 | 116 | 117 | ******************* 118 | Using event filters 119 | ******************* 120 | 121 | If your Ethereum Node supports log filters, you can activate it in the Django settings and it will use filters instead of iterating thru all blocks and all transactions. 122 | 123 | .. code-block:: python 124 | 125 | ETHEREUM_LOGS_FILTER_AVAILABLE = True 126 | 127 | 128 | 129 | ****************************** 130 | More about the event receivers 131 | ****************************** 132 | 133 | It is advisable that the code inside the custom event receiver to be simple since it is run synchronously while the ``event_listener`` task is running. If that is not the case, pass the argument ``decoded_event`` to a celery task of your own 134 | 135 | .. code-block:: python 136 | 137 | # inside the CustomEventReceiver.save method 138 | from django_ethereum_events.utils import HexJsonEncoder 139 | decoded_event_data = json.dumps(decoded_event, cls=HexJsonEncoder) 140 | my_custom_task.delay(decoded_event_data) 141 | 142 | 143 | If an unhandled exception is raised inside the event receiver, the ``event_listener`` task logs the error and creates 144 | a new instance of the ``django_ethereum_events.models.FailedEventLog`` containing all the relevant event information. 145 | 146 | The event listener does **not** attempt to rerun ``FailedEventLogs``. That is up to the client implementation. 147 | 148 | 149 | **************************** 150 | Resetting the internal state 151 | **************************** 152 | Blocks are processed only once. The last block processed is stored in the ``.models.Daemon`` entry. 153 | 154 | To reset the number of blocks processed, run the ``reset_block_daemon`` command optionally specifying the block number (-b, --block) to reset to (defaults to zero). If you reset it to zero, the next time the ``event_listener`` is fired, it will start processing blocks from the genesis block. 155 | 156 | The ``Daemon`` entry can also be changed from the django admin backend. 157 | 158 | *************************** 159 | Proof-of-Authority Networks 160 | *************************** 161 | To use this package on **Rinkeby** or any other private network that uses the Proof-of-Authority consensus engine (also named clique), set the optional ``ETHEREUM_GETH_POA`` setting to ``True``. 162 | -------------------------------------------------------------------------------- /django_ethereum_events/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'django_ethereum_events.apps.EthereumEventsConfig' 2 | -------------------------------------------------------------------------------- /django_ethereum_events/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from solo.admin import SingletonModelAdmin 4 | 5 | from .forms import MonitoredEventForm 6 | from .models import Daemon, FailedEventLog, MonitoredEvent 7 | 8 | admin.site.register(Daemon, SingletonModelAdmin) 9 | 10 | 11 | class MonitoredEventAdmin(admin.ModelAdmin): 12 | list_display = ['id', 'name', 'contract_address', 'topic', 'event_receiver', 'monitored_from'] 13 | list_filter = ['contract_address'] 14 | search_fields = ['name', 'contract_address'] 15 | 16 | add_form = MonitoredEventForm 17 | 18 | def get_form(self, request, obj=None, **kwargs): 19 | defaults = {} 20 | if obj is None: 21 | defaults['form'] = self.add_form 22 | defaults.update(kwargs) 23 | return super(MonitoredEventAdmin, self).get_form(request, obj, **defaults) 24 | 25 | 26 | admin.site.register(MonitoredEvent, MonitoredEventAdmin) 27 | 28 | 29 | class FailedEventLogAdmin(admin.ModelAdmin): 30 | list_display = ['id', 'event', 'block_number', 'log_index', 'address', 'transaction_hash', 'created'] 31 | list_filter = ['address'] 32 | search_fields = ['event'] 33 | 34 | 35 | admin.site.register(FailedEventLog, FailedEventLogAdmin) 36 | -------------------------------------------------------------------------------- /django_ethereum_events/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EthereumEventsConfig(AppConfig): 5 | name = 'django_ethereum_events' 6 | 7 | def ready(self): 8 | super().ready() 9 | 10 | import django_ethereum_events.signals # noqa -------------------------------------------------------------------------------- /django_ethereum_events/chainevents.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | try: 4 | # Django 1.11.x - 2.x 5 | from django.utils.six import with_metaclass 6 | except ImportError: 7 | # Django 3.x 8 | from six import with_metaclass 9 | 10 | 11 | class AbstractEventReceiver(with_metaclass(ABCMeta)): 12 | """Abstract EventReceiver class. 13 | 14 | For every Event that is monitored, an Event handler that inherits 15 | this class must be created and the `save` method must be implemented. 16 | """ 17 | 18 | @abstractmethod 19 | def save(self, decoded_event): 20 | pass 21 | -------------------------------------------------------------------------------- /django_ethereum_events/decoder.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from web3 import Web3 4 | from web3._utils.events import get_event_data 5 | 6 | from django_ethereum_events.models import MonitoredEvent 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Decoder: 12 | """Event log decoder. 13 | 14 | Attributes: 15 | monitored_events (QuerySet): retrieved monitored events 16 | monitored_events: dict (address, topic) => monitored_event 17 | 18 | """ 19 | 20 | monitored_events = None 21 | watched_addresses = [] 22 | topics = {} 23 | 24 | def __init__(self, block_number, *args, **kwargs): 25 | super(Decoder, self).__init__(*args, **kwargs) 26 | self.refresh_state(block_number) 27 | self.web3 = Web3() 28 | 29 | def refresh_state(self, block_number): 30 | """Fetches the monitored events from the database and updates the decoder state variables. 31 | 32 | Args: 33 | block_number (int): next block to process 34 | 35 | """ 36 | self.watched_addresses.clear() 37 | self.topics.clear() 38 | self.monitored_events = {} # dict (address, topic) => [monitored_event1, monitored_event2, ...] 39 | 40 | for monitored_event in MonitoredEvent.objects.all(): 41 | self.monitored_events[ 42 | (monitored_event.contract_address, monitored_event.topic) 43 | ] = monitored_event 44 | 45 | if monitored_event.monitored_from is None: 46 | monitored_event.monitored_from = block_number 47 | monitored_event.save() 48 | 49 | def decode_log(self, log): 50 | """ 51 | Decodes a retrieved relevant log. 52 | 53 | Decoding is performed with the `web3.utils.events.get_event_data` 54 | function. 55 | 56 | Args: 57 | log (AttributeDict): the event log to decode 58 | Returns: 59 | The decoded log. 60 | 61 | """ 62 | log_topic = log['topics'][0].hex() 63 | address = log['address'] 64 | mon_evt = self.monitored_events.get((address, log_topic), None) 65 | if mon_evt is None: 66 | return None # combination of (address, topic) not monitored 67 | event_abi = mon_evt.event_abi_parsed 68 | decoded_log = get_event_data(self.web3.codec, event_abi, log) 69 | return (address, log_topic), decoded_log 70 | 71 | def decode_logs(self, logs): 72 | """Decode the given logs. 73 | 74 | Args: 75 | logs (list): The targeted logs to decode. 76 | Returns: 77 | list: the decoded logs 78 | 79 | """ 80 | ret = [] 81 | for log in logs: 82 | decoded = self.decode_log(log) 83 | if decoded is not None: 84 | ret.append(decoded) 85 | return ret 86 | -------------------------------------------------------------------------------- /django_ethereum_events/event_listener.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import json 3 | import logging 4 | 5 | from django.conf import settings 6 | from django.core.cache import cache 7 | from django.utils.module_loading import import_string 8 | 9 | from .decoder import Decoder 10 | from .exceptions import UnknownBlock 11 | from .models import CACHE_UPDATE_KEY, Daemon, FailedEventLog 12 | from .utils import HexJsonEncoder, refresh_cache_update_value 13 | from .web3_service import Web3Service 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class EventListener: 19 | """Event Listener class.""" 20 | 21 | def __init__(self, *args, **kwargs): 22 | super(EventListener, self).__init__() 23 | self.daemon = Daemon.get_solo() 24 | self.decoder = Decoder(block_number=self.daemon.block_number + 1) 25 | self.web3 = Web3Service(*args, **kwargs).web3 26 | 27 | def _get_block_range(self): 28 | current = self.web3.eth.blockNumber 29 | step = getattr(settings, "ETHEREUM_LOGS_BATCH_SIZE", 10000) 30 | if self.daemon.block_number < current: 31 | start = self.daemon.block_number + 1 32 | return start, min(current, start + step) 33 | 34 | return None, None 35 | 36 | def get_pending_blocks(self): 37 | """ 38 | Retrieve the blocks that have not been processed. 39 | 40 | Returns: 41 | An iterable of (from, to) tuples, containing 42 | the unprocessed block numbers. 43 | 44 | """ 45 | start, end = self._get_block_range() 46 | if start is None: 47 | return [] 48 | else: 49 | return list(range(start, end + 1)) 50 | 51 | def update_block_number(self, block_number): 52 | """Updates the internal block_number counter.""" 53 | self.daemon.block_number = block_number 54 | self.daemon.save() 55 | 56 | def get_block_logs(self, block_number): 57 | """Retrieves the relevant log entries from the given block. 58 | 59 | Args: 60 | block_number (int): The block number of the block to process. 61 | Returns: 62 | The list of relevant log entries. 63 | 64 | """ 65 | block = self.web3.eth.getBlock(block_number) 66 | relevant_logs = [] 67 | if block and block.get('hash'): 68 | for tx in block['transactions']: 69 | receipt = self.web3.eth.getTransactionReceipt(tx) 70 | 71 | if receipt is None: 72 | continue 73 | 74 | for log in receipt.get('logs', []): 75 | address = log['address'] 76 | topic = log['topics'][0].hex() 77 | if (address, topic) in self.decoder.monitored_events: 78 | relevant_logs.append(log) 79 | return relevant_logs 80 | else: 81 | raise UnknownBlock 82 | 83 | def get_logs(self, from_block, to_block): 84 | """ 85 | Retrieves the relevant log entries from the given block range. 86 | 87 | Args: 88 | from_block (int): The first block number. 89 | to_block (int): The last block number. 90 | 91 | Returns: 92 | The list of relevant log entries. 93 | 94 | """ 95 | logs = itertools.chain.from_iterable( 96 | self.get_block_logs(n) for n in range(from_block, to_block + 1)) 97 | return list(logs) 98 | 99 | def save_events(self, decoded_logs): 100 | """ 101 | Fires the appropriate event receivers for every given log. 102 | 103 | Args: 104 | decoded_logs (:obj:`list` of :obj:`dict`): The decoded logs. 105 | 106 | """ 107 | for (address, topic), decoded_log in decoded_logs: 108 | event_receiver = self.decoder.monitored_events[(address, topic)].event_receiver 109 | 110 | try: 111 | event_receiver_cls = import_string(event_receiver) 112 | event_receiver_cls().save(decoded_event=decoded_log) 113 | except Exception: 114 | # Save the event information that caused the exception 115 | failed_event = FailedEventLog.objects.create( 116 | event=decoded_log.event, 117 | transaction_hash=decoded_log.transactionHash.hex(), 118 | transaction_index=decoded_log.transactionIndex, 119 | block_hash=decoded_log.blockHash.hex(), 120 | block_number=decoded_log.blockNumber, 121 | log_index=decoded_log.logIndex, 122 | address=decoded_log.address, 123 | args=json.dumps(decoded_log.args, cls=HexJsonEncoder), 124 | monitored_event=self.decoder.monitored_events[(address, topic)] 125 | ) 126 | 127 | logger.error('Exception while calling {0}. FailedEventLog entry with id {1} created.'.format( 128 | event_receiver, failed_event.pk), exc_info=True) 129 | 130 | def check_for_state_updates(self, block_number): 131 | """If a MonitoredEvent has been added, updated, deleted, the decoder state is updated. 132 | 133 | Args: 134 | block_number: current working block 135 | 136 | """ 137 | update_required = cache.get(CACHE_UPDATE_KEY, False) 138 | if update_required: 139 | self.decoder.refresh_state(block_number=block_number) 140 | refresh_cache_update_value(update_required=False) 141 | 142 | def execute(self): 143 | """Program loop, does all the underlying work.""" 144 | if getattr(settings, "ETHEREUM_LOGS_FILTER_AVAILABLE", False): 145 | self._execute_using_filters() 146 | else: 147 | self._execute_iterating_all_blocks() 148 | 149 | def _execute_using_filters(self): 150 | """Uses filters to fetch required logs""" 151 | start, end = self._get_block_range() 152 | if start is None: 153 | return 154 | all_logs = [] 155 | 156 | for (address, topic) in self.decoder.monitored_events.keys(): 157 | log_filter = self.web3.eth.filter({ 158 | "topics": [topic], 159 | "address": address, 160 | "fromBlock": start, 161 | "toBlock": end, 162 | }) 163 | all_logs.extend(log_filter.get_all_entries()) 164 | 165 | all_logs.sort(key=lambda log: (log["blockNumber"], log["logIndex"])) 166 | decoded_logs = self.decoder.decode_logs(all_logs) 167 | self.save_events(decoded_logs) 168 | self.update_block_number(end) 169 | 170 | def _execute_iterating_all_blocks(self): 171 | """Executes iterating thru all blocks and txs""" 172 | pending_blocks = self.get_pending_blocks() 173 | for block in pending_blocks: 174 | self.check_for_state_updates(block) 175 | logs = self.get_block_logs(block) 176 | decoded_logs = self.decoder.decode_logs(logs) 177 | self.save_events(decoded_logs) 178 | self.update_block_number(block) 179 | -------------------------------------------------------------------------------- /django_ethereum_events/exceptions.py: -------------------------------------------------------------------------------- 1 | class UnknownBlock(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /django_ethereum_events/forms.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | 4 | from django import forms 5 | from django.forms import widgets 6 | from django.utils.module_loading import import_string 7 | try: 8 | from django.utils.translation import ugettext_lazy as _ 9 | except ImportError: 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from eth_utils import add_0x_prefix, event_abi_to_log_topic, is_hex_address 13 | 14 | from web3 import Web3 15 | 16 | from .chainevents import AbstractEventReceiver 17 | from .models import MonitoredEvent 18 | from .utils import get_event_abi 19 | 20 | 21 | class MonitoredEventForm(forms.ModelForm): 22 | name = forms.CharField(max_length=256) 23 | contract_address = forms.CharField(max_length=42, min_length=42) 24 | contract_abi = forms.Field(widget=widgets.Textarea) 25 | event_receiver = forms.CharField(max_length=256) 26 | 27 | class Meta: 28 | model = MonitoredEvent 29 | fields = ('name', 'contract_address', 'event_receiver') 30 | 31 | def clean_contract_address(self): 32 | contract_address = self.cleaned_data['contract_address'] 33 | 34 | if not is_hex_address(contract_address): 35 | raise forms.ValidationError( 36 | _('Contract address %(address)s is not a valid hex address') % {'address': contract_address}) 37 | return Web3.toChecksumAddress(contract_address) 38 | 39 | def clean_contract_abi(self): 40 | contract_abi = self.cleaned_data['contract_abi'] 41 | is_str = isinstance(contract_abi, str) 42 | is_list = isinstance(contract_abi, list) 43 | 44 | if not (is_str or is_list): 45 | raise forms.ValidationError(_('Contract abi must be either `str` or `list`')) 46 | 47 | if is_str: 48 | try: 49 | contract_abi = json.loads(contract_abi) 50 | except Exception: 51 | raise forms.ValidationError(_('Invalid contract abi')) 52 | 53 | return contract_abi 54 | 55 | def clean_event_receiver(self): 56 | event_receiver = self.cleaned_data['event_receiver'] 57 | 58 | try: 59 | event_handler_cls = import_string(event_receiver) 60 | if not inspect.isclass(event_handler_cls) or not issubclass(event_handler_cls, AbstractEventReceiver): 61 | raise forms.ValidationError( 62 | _('%(receiver)s is not a valid subclass of AbstractEventReceiver') % {'receiver': event_receiver}) 63 | except ImportError: 64 | raise forms.ValidationError(_('Cannot import module %(module)s') % {'module': event_receiver}) 65 | 66 | return event_receiver 67 | 68 | def clean(self): 69 | cleaned_data = super().clean() 70 | 71 | name = cleaned_data.get('name') 72 | abi = cleaned_data.get('contract_abi') 73 | 74 | try: 75 | self._event_abi = get_event_abi(abi, name) 76 | except ValueError: 77 | raise forms.ValidationError(_('Event %(name)s cannot be found in the given contract ABI') % {'name': name}) 78 | 79 | def save(self, commit=True): 80 | event = super(MonitoredEventForm, self).save(commit=False) # event is not ready to be saved 81 | 82 | topic = event_abi_to_log_topic(self._event_abi) 83 | event.topic = add_0x_prefix(topic.hex()) 84 | event.event_abi = json.dumps(self._event_abi) 85 | event.save() 86 | 87 | return event 88 | -------------------------------------------------------------------------------- /django_ethereum_events/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/django_ethereum_events/management/__init__.py -------------------------------------------------------------------------------- /django_ethereum_events/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/django_ethereum_events/management/commands/__init__.py -------------------------------------------------------------------------------- /django_ethereum_events/management/commands/reset_block_daemon.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | 3 | from django_ethereum_events.models import Daemon 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Resets the internal ethereum block number counter.' 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument( 11 | '-b', 12 | '--block', 13 | nargs='?', 14 | type=int, 15 | action='store', 16 | dest='block', 17 | default=0, 18 | help='Block number to reset the counter to' 19 | ) 20 | 21 | def handle(self, *args, **options): 22 | block_number = options['block'] 23 | d = Daemon.get_solo() 24 | d.block_number = block_number 25 | d.last_error_block_number = 0 26 | d.save() 27 | 28 | self.stdout.write(self.style.SUCCESS( 29 | 'Internal block number counter reset to {0}.'.format(block_number) 30 | )) 31 | -------------------------------------------------------------------------------- /django_ethereum_events/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-25 08:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Daemon', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, 20 | primary_key=True, serialize=False, verbose_name='ID')), 21 | ('block_number', models.IntegerField(default=0)), 22 | ('last_error_block_number', models.IntegerField(default=0)), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ('modified', models.DateTimeField(auto_now=True)), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /django_ethereum_events/migrations/0002_auto_20180531_0806.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-31 08:06 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_ethereum_events', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='MonitoredEvent', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=256)), 19 | ('contract_address', models.CharField(max_length=42, validators=[django.core.validators.MinLengthValidator(42)])), 20 | ('topic', models.CharField(max_length=64, validators=[django.core.validators.MinLengthValidator(64)])), 21 | ('event_receiver', models.CharField(max_length=256)), 22 | ('monitored_from', models.IntegerField(blank=True, help_text='Block number in which monitoring for this event started', null=True)), 23 | ], 24 | options={ 25 | 'verbose_name_plural': 'Monitored Events', 26 | 'verbose_name': 'Monitored Event', 27 | }, 28 | ), 29 | migrations.AlterUniqueTogether( 30 | name='monitoredevent', 31 | unique_together={('topic', 'contract_address')}, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_ethereum_events/migrations/0003_auto_20180531_0836.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-31 08:36 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_ethereum_events', '0002_auto_20180531_0806'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='monitoredevent', 16 | name='event_abi', 17 | field=models.TextField(default=''), 18 | preserve_default=False, 19 | ), 20 | migrations.AlterField( 21 | model_name='monitoredevent', 22 | name='topic', 23 | field=models.CharField(max_length=66, validators=[django.core.validators.MinLengthValidator(66)]), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /django_ethereum_events/migrations/0004_failedeventlog.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-06-01 14:42 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_ethereum_events', '0003_auto_20180531_0836'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='FailedEventLog', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('event', models.CharField(max_length=256)), 20 | ('transaction_hash', models.CharField(max_length=66, validators=[django.core.validators.MinLengthValidator(66)])), 21 | ('transaction_index', models.IntegerField()), 22 | ('block_hash', models.CharField(max_length=66, validators=[django.core.validators.MinLengthValidator(66)])), 23 | ('block_number', models.IntegerField()), 24 | ('log_index', models.IntegerField()), 25 | ('address', models.CharField(max_length=42, validators=[django.core.validators.MinLengthValidator(42)])), 26 | ('args', models.TextField(default='{}')), 27 | ('created', models.DateTimeField(auto_now_add=True)), 28 | ('monitored_event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='failed_events', to='django_ethereum_events.MonitoredEvent')), 29 | ], 30 | options={ 31 | 'verbose_name_plural': 'Failed to process Events', 32 | 'verbose_name': 'Failed to process Event', 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /django_ethereum_events/migrations/0005_auto_20180713_1130.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-07-13 11:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_ethereum_events', '0004_failedeventlog'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='daemon', 15 | name='block_number', 16 | field=models.IntegerField(default=0, help_text='Last block processed'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_ethereum_events/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/django_ethereum_events/migrations/__init__.py -------------------------------------------------------------------------------- /django_ethereum_events/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.validators import MinLengthValidator 4 | from django.db import models 5 | try: 6 | from django.utils.translation import ugettext_lazy as _ 7 | except ImportError: 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from solo.models import SingletonModel 11 | 12 | CACHE_UPDATE_KEY = '_django_ethereum_events_update_required' 13 | 14 | 15 | class Daemon(SingletonModel): 16 | """Model responsible for storing blockchain related information.""" 17 | 18 | block_number = models.IntegerField(default=0, help_text=_('Last block processed')) 19 | last_error_block_number = models.IntegerField(default=0) 20 | created = models.DateTimeField(auto_now_add=True) 21 | modified = models.DateTimeField(auto_now=True) 22 | 23 | 24 | class EventManager(models.Manager): 25 | """Model manager for MonitoredEvent model.""" 26 | 27 | @staticmethod 28 | def register_event(event_name, contract_address, contract_abi, event_receiver): 29 | """Helper function that creates a new MonitoredEvent. 30 | 31 | Args: 32 | event_name (str): the name of the Event that is been emitted 33 | contract_address (str): the address of the contract emitting the event (hexstring) 34 | contract_abi (obj): the contract abi either as `str` or `dict` 35 | event_receiver (str): module in which the event information is passed, must be importable 36 | 37 | Returns: 38 | The created MonitoredEvent object 39 | 40 | Raises: 41 | ValueError if any of the above fields are malformed. 42 | """ 43 | from .forms import MonitoredEventForm 44 | form = MonitoredEventForm({ 45 | 'name': event_name, 46 | 'contract_address': contract_address, 47 | 'event_receiver': event_receiver, 48 | 'contract_abi': contract_abi 49 | }) 50 | 51 | if form.is_valid(): 52 | event = form.save() 53 | return event 54 | 55 | raise ValueError('The following arguments are invalid \n{0}'.format(form.errors.as_text())) 56 | 57 | 58 | class MonitoredEvent(models.Model): 59 | """Holds the events that are currently monitored on the blockchain.""" 60 | 61 | name = models.CharField(max_length=256) 62 | contract_address = models.CharField(max_length=42, validators=[MinLengthValidator(42)]) 63 | event_abi = models.TextField() 64 | topic = models.CharField(max_length=66, validators=[MinLengthValidator(66)]) 65 | event_receiver = models.CharField(max_length=256) 66 | monitored_from = models.IntegerField(blank=True, null=True, 67 | help_text=_('Block number in which monitoring for this event started')) 68 | 69 | objects = EventManager() 70 | 71 | class Meta: 72 | verbose_name = _('Monitored Event') 73 | verbose_name_plural = _('Monitored Events') 74 | unique_together = ('topic', 'contract_address') 75 | 76 | def __str__(self): 77 | return '{0} at {1}'.format(self.name, self.contract_address) 78 | 79 | @property 80 | def event_abi_parsed(self): 81 | if hasattr(self, '_event_abi_parsed'): 82 | return self._event_abi_parsed 83 | 84 | self._event_abi_parsed = json.loads(self.event_abi) 85 | return self._event_abi_parsed 86 | 87 | 88 | class FailedEventLog(models.Model): 89 | """This model holds the event logs that raised an Exception inside the client's event_receiver method. 90 | 91 | When a decode log that is passed inside the client's implementation of the `AbstractEventReceiver` 92 | raises an exception, the `EventListener` is not halted. Instead, the event log that caused 93 | the unhandled expeption is stored in this model, along with all the information for the user to 94 | `replay` the invocation of the custom `event_receiver` implementation. 95 | """ 96 | 97 | event = models.CharField(max_length=256) 98 | transaction_hash = models.CharField(max_length=66, validators=[MinLengthValidator(66)]) 99 | transaction_index = models.IntegerField() 100 | block_hash = models.CharField(max_length=66, validators=[MinLengthValidator(66)]) 101 | block_number = models.IntegerField() 102 | log_index = models.IntegerField() 103 | address = models.CharField(max_length=42, validators=[MinLengthValidator(42)]) 104 | args = models.TextField(default="{}") # noqa: P103 105 | monitored_event = models.ForeignKey(MonitoredEvent, related_name='failed_events', on_delete=models.CASCADE) 106 | created = models.DateTimeField(auto_now_add=True) 107 | 108 | class Meta: 109 | verbose_name = _('Failed to process Event') 110 | verbose_name_plural = _('Failed to process Events') 111 | 112 | def __str__(self): 113 | return self.event 114 | -------------------------------------------------------------------------------- /django_ethereum_events/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/django_ethereum_events/settings/__init__.py -------------------------------------------------------------------------------- /django_ethereum_events/settings/dev.py: -------------------------------------------------------------------------------- 1 | import json 2 | from os import pardir, path 3 | 4 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 5 | BASE_DIR = path.dirname(path.dirname( 6 | path.abspath(path.join(__file__, pardir)) 7 | )) 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = 'wm82)$7ay(ya#g788(4s#hq1w2cynlt$+2ay_noapdj-#6h7xl' 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | DEBUG = True 14 | 15 | INSTALLED_APPS = ( 16 | 'django.contrib.admin', 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.messages', 21 | 'django.contrib.staticfiles', 22 | 23 | 'django_ethereum_events', 24 | 'solo', 25 | 'dev' 26 | ) 27 | 28 | CACHES = { 29 | 'default': { 30 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 31 | } 32 | } 33 | 34 | DATABASES = { 35 | 'default': { 36 | 'ENGINE': 'django.db.backends.sqlite3', 37 | 'NAME': path.join(BASE_DIR, 'db.sqlite3'), 38 | } 39 | } 40 | 41 | CELERY_ALWAYS_EAGER = True 42 | 43 | ROOT_URLCONF = 'django_ethereum_events.urls' 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [] 60 | , 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | LANGUAGE_CODE = 'en-us' 74 | TIME_ZONE = 'UTC' 75 | USE_I18N = True 76 | USE_L10N = True 77 | USE_TZ = True 78 | 79 | 80 | # Static files (CSS, JavaScript, Images) 81 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 82 | STATIC_ROOT = path.join(BASE_DIR, 'static') 83 | STATIC_URL = '/static/' 84 | 85 | ETHEREUM_NODE_HOST = 'localhost' 86 | ETHEREUM_NODE_PORT = 8545 87 | ETHEREUM_NODE_SSL = False -------------------------------------------------------------------------------- /django_ethereum_events/settings/test.py: -------------------------------------------------------------------------------- 1 | import json 2 | from os import pardir, path 3 | 4 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 5 | BASE_DIR = path.dirname(path.dirname( 6 | path.abspath(path.join(__file__, pardir)) 7 | )) 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = 'wm82)$7ay(ya#g788(4s#hq1w2cynlt$+2ay_noapdj-#6h7xl' 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | DEBUG = True 14 | 15 | INSTALLED_APPS = ( 16 | 'django_ethereum_events', 17 | 'solo' 18 | ) 19 | 20 | CACHES = { 21 | 'default': { 22 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 23 | } 24 | } 25 | 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'django.db.backends.sqlite3', 29 | 'NAME': path.join(BASE_DIR, 'db.sqlite3'), 30 | } 31 | } 32 | 33 | CELERY_ALWAYS_EAGER = True 34 | 35 | -------------------------------------------------------------------------------- /django_ethereum_events/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_delete, post_save 2 | from django.dispatch import receiver 3 | 4 | from .models import MonitoredEvent 5 | from .utils import refresh_cache_update_value 6 | 7 | 8 | @receiver(post_save, sender=MonitoredEvent) 9 | def monitored_event_created_or_updated(**kwargs): 10 | refresh_cache_update_value(update_required=True) 11 | 12 | 13 | @receiver(post_delete, sender=MonitoredEvent) 14 | def monitored_event_deleted(**kwargs): 15 | refresh_cache_update_value(update_required=True) 16 | -------------------------------------------------------------------------------- /django_ethereum_events/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import contextmanager 3 | 4 | from celery import shared_task 5 | 6 | from django.core.cache import cache 7 | 8 | from .event_listener import EventListener 9 | 10 | 11 | LOCK_KEY = '_django_ethereum_events_cache_lock' 12 | LOCK_VALUE = 'LOCK' 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | @contextmanager 17 | def cache_lock(lock_id, lock_value): 18 | """Cache based locking mechanism. 19 | 20 | Cache backends `memcached` and `redis` are recommended. 21 | """ 22 | # cache.add fails if the key already exists 23 | status = cache.add(lock_id, lock_value) 24 | try: 25 | yield status 26 | finally: 27 | if status: 28 | cache.delete(lock_id) 29 | 30 | 31 | @shared_task 32 | def event_listener(): 33 | """ 34 | Celery task that transverses the blockchain looking for event logs. 35 | 36 | This task should be run periodically via celerybeat to monitor for 37 | new blocks in the blockchain. 38 | 39 | Examples: 40 | CELERYBEAT_SCHEDULE = { 41 | 'ethereum_events': { 42 | 'task': 'django_ethereum_events.tasks.event_listener', 43 | 'schedule': crontab(minute='*/2') # run every 2 minutes 44 | } 45 | } 46 | 47 | """ 48 | with cache_lock(LOCK_KEY, LOCK_VALUE) as acquired: 49 | if acquired: 50 | listener = EventListener() 51 | try: 52 | listener.execute() 53 | except Exception: 54 | logger.exception('Exception while running event listener task', exc_info=True) 55 | daemon = listener.daemon 56 | last_processed_block = daemon.block_number 57 | daemon.last_error_block_number = last_processed_block + 1 58 | daemon.save() 59 | else: 60 | logger.info('Event listener is already running. Skipping execution.') 61 | -------------------------------------------------------------------------------- /django_ethereum_events/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/django_ethereum_events/tests/__init__.py -------------------------------------------------------------------------------- /django_ethereum_events/tests/contracts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/django_ethereum_events/tests/contracts/__init__.py -------------------------------------------------------------------------------- /django_ethereum_events/tests/contracts/bank.py: -------------------------------------------------------------------------------- 1 | BANK_SOURCE = """ 2 | pragma solidity ^0.4.22; 3 | 4 | contract Bank { 5 | mapping (address => uint) balances; 6 | 7 | event LogWithdraw(address indexed owner, uint amount); 8 | event LogDeposit(address indexed owner, uint amount); 9 | 10 | constructor() public payable {} 11 | 12 | function withdraw(uint amount) public { 13 | require(balances[msg.sender] >= amount); 14 | balances[msg.sender] -= amount; 15 | msg.sender.transfer(amount); 16 | emit LogWithdraw(msg.sender, amount); 17 | } 18 | 19 | function deposit() public payable { 20 | balances[msg.sender] += msg.value; 21 | emit LogDeposit(msg.sender, msg.value); 22 | } 23 | 24 | function getBalance() public view returns (uint) { 25 | return balances[msg.sender]; 26 | } 27 | } 28 | """ 29 | 30 | BANK_BYTECODE = "60806040526102fd806100136000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806312065fe01461005c5780632e1a7d4d14610087578063d0e30db0146100b4575b600080fd5b34801561006857600080fd5b506100716100be565b6040518082815260200191505060405180910390f35b34801561009357600080fd5b506100b260048036038101908080359060200190929190505050610104565b005b6100bc610235565b005b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905090565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561015157600080fd5b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501580156101e3573d6000803e3d6000fd5b503373ffffffffffffffffffffffffffffffffffffffff167f4ce7033d118120e254016dccf195288400b28fc8936425acd5f17ce2df3ab708826040518082815260200191505060405180910390a250565b346000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167f1b851e1031ef35a238e6c67d0c7991162390df915f70eaf9098dbf0b175a6198346040518082815260200191505060405180910390a25600a165627a7a723058207a9042b8bc4ad16515236e357784320ed78b4b74c0102cff6a8f61d11cebe5250029" 31 | 32 | BANK_ABI_RAW = """ 33 | [ 34 | { 35 | "constant": false, 36 | "inputs": [], 37 | "name": "deposit", 38 | "outputs": [], 39 | "payable": true, 40 | "stateMutability": "payable", 41 | "type": "function" 42 | }, 43 | { 44 | "anonymous": false, 45 | "inputs": [ 46 | { 47 | "indexed": true, 48 | "name": "owner", 49 | "type": "address" 50 | }, 51 | { 52 | "indexed": false, 53 | "name": "amount", 54 | "type": "uint256" 55 | } 56 | ], 57 | "name": "LogWithdraw", 58 | "type": "event" 59 | }, 60 | { 61 | "anonymous": false, 62 | "inputs": [ 63 | { 64 | "indexed": true, 65 | "name": "owner", 66 | "type": "address" 67 | }, 68 | { 69 | "indexed": false, 70 | "name": "amount", 71 | "type": "uint256" 72 | } 73 | ], 74 | "name": "LogDeposit", 75 | "type": "event" 76 | }, 77 | { 78 | "inputs": [], 79 | "payable": true, 80 | "stateMutability": "payable", 81 | "type": "constructor" 82 | }, 83 | { 84 | "constant": false, 85 | "inputs": [ 86 | { 87 | "name": "amount", 88 | "type": "uint256" 89 | } 90 | ], 91 | "name": "withdraw", 92 | "outputs": [], 93 | "payable": false, 94 | "stateMutability": "nonpayable", 95 | "type": "function" 96 | }, 97 | { 98 | "constant": true, 99 | "inputs": [], 100 | "name": "getBalance", 101 | "outputs": [ 102 | { 103 | "name": "", 104 | "type": "uint256" 105 | } 106 | ], 107 | "payable": false, 108 | "stateMutability": "view", 109 | "type": "function" 110 | } 111 | ] 112 | """ 113 | -------------------------------------------------------------------------------- /django_ethereum_events/tests/contracts/claim.py: -------------------------------------------------------------------------------- 1 | CLAIM_SOURCE = """ 2 | pragma solidity ^0.4.22; 3 | 4 | contract Claim { 5 | mapping (address => mapping (bytes32 => bytes32)) claims; 6 | 7 | event ClaimSet(address indexed owner, bytes32 key, bytes32 value); 8 | 9 | function setClaim(bytes32 key, bytes32 value) public { 10 | claims[msg.sender][key] = value; 11 | emit ClaimSet(msg.sender, key, value); 12 | } 13 | 14 | function getClaim(bytes32 key) public view returns (bytes32) { 15 | return claims[msg.sender][key]; 16 | } 17 | } 18 | """ 19 | 20 | CLAIM_BYTECODE = "608060405234801561001057600080fd5b50610234806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c9100bcb14610051578063d8d98c7b1461009e575b600080fd5b34801561005d57600080fd5b5061008060048036038101908080356000191690602001909291905050506100dd565b60405180826000191660001916815260200191505060405180910390f35b3480156100aa57600080fd5b506100db6004803603810190808035600019169060200190929190803560001916906020019092919050505061013e565b005b60008060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008360001916600019168152602001908152602001600020549050919050565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000846000191660001916815260200190815260200160002081600019169055503373ffffffffffffffffffffffffffffffffffffffff167f32e23ce2e82f46f421b8ffb2c7c2e2fa3db44ade2ac5f6796f52db108a887c5b838360405180836000191660001916815260200182600019166000191681526020019250505060405180910390a250505600a165627a7a72305820c63effc0a159f423f82dea3b99dadec63f2f1c069e06043d538fb09850eb41710029" 21 | 22 | CLAIM_ABI_RAW = """ 23 | [ 24 | { 25 | "anonymous": false, 26 | "inputs": [ 27 | { 28 | "indexed": true, 29 | "name": "owner", 30 | "type": "address" 31 | }, 32 | { 33 | "indexed": false, 34 | "name": "key", 35 | "type": "bytes32" 36 | }, 37 | { 38 | "indexed": false, 39 | "name": "value", 40 | "type": "bytes32" 41 | } 42 | ], 43 | "name": "ClaimSet", 44 | "type": "event" 45 | }, 46 | { 47 | "constant": false, 48 | "inputs": [ 49 | { 50 | "name": "key", 51 | "type": "bytes32" 52 | }, 53 | { 54 | "name": "value", 55 | "type": "bytes32" 56 | } 57 | ], 58 | "name": "setClaim", 59 | "outputs": [], 60 | "payable": false, 61 | "stateMutability": "nonpayable", 62 | "type": "function" 63 | }, 64 | { 65 | "constant": true, 66 | "inputs": [ 67 | { 68 | "name": "key", 69 | "type": "bytes32" 70 | } 71 | ], 72 | "name": "getClaim", 73 | "outputs": [ 74 | { 75 | "name": "", 76 | "type": "bytes32" 77 | } 78 | ], 79 | "payable": false, 80 | "stateMutability": "view", 81 | "type": "function" 82 | } 83 | ] 84 | """ -------------------------------------------------------------------------------- /django_ethereum_events/tests/deposit_event_log.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", 4 | "blockNumber": 3, 5 | "logIndex": 0, 6 | "address": "0xc305c901078781C232A2a521C2aF7980f8385ee9", 7 | "blockHash": "0xe88e56bceb2125a6fee3f779cf1d0b292ea0e6e7031675758841af087f5a6bd1", 8 | "transactionIndex": 0, 9 | "transactionHash": "0xffb7b9b00d1328cbab20c8a15fd8b3b17303421f175020904e04c6d372434e9a", 10 | "type": "mined", 11 | "topics": [ 12 | "0x1b851e1031ef35a238e6c67d0c7991162390df915f70eaf9098dbf0b175a6198", 13 | "0x00000000000000000000000082a978b3f5962a5b0957d9ee9eef472ee55b42f1" 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /django_ethereum_events/tests/test_decoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | 4 | from django.test import TestCase 5 | from eth_tester import EthereumTester, PyEVMBackend 6 | from eth_utils import to_wei 7 | from hexbytes import HexBytes 8 | from web3 import EthereumTesterProvider, Web3 9 | 10 | from django_ethereum_events.models import MonitoredEvent 11 | from django_ethereum_events.tests.contracts.bank import BANK_ABI_RAW, BANK_BYTECODE 12 | from ..decoder import Decoder 13 | 14 | 15 | class DecoderTestCase(TestCase): 16 | events_log_file = 'django_ethereum_events/tests/deposit_event_log.json' 17 | 18 | def setUp(self): 19 | TestCase.setUp(self) 20 | 21 | def tearDown(self): 22 | super(DecoderTestCase, self).tearDown() 23 | 24 | # Reset to snapshot 25 | self.eth_tester.revert_to_snapshot(self.clean_state_snapshot) 26 | 27 | @classmethod 28 | def setUpTestData(cls): 29 | super(DecoderTestCase, cls).setUpTestData() 30 | 31 | cls.eth_tester = EthereumTester(backend=PyEVMBackend()) 32 | cls.provider = EthereumTesterProvider(cls.eth_tester) 33 | cls.web3 = Web3(cls.provider) 34 | 35 | # Deploy the Bank test contract 36 | cls.bank_abi = json.loads(BANK_ABI_RAW) 37 | bank_bytecode = BANK_BYTECODE 38 | 39 | Bank = cls.web3.eth.contract(abi=cls.bank_abi, bytecode=bank_bytecode) 40 | tx_hash = Bank.constructor().transact() 41 | tx_receipt = cls.web3.eth.waitForTransactionReceipt(tx_hash) 42 | cls.bank_address = tx_receipt.contractAddress 43 | cls.bank_contract = cls.web3.eth.contract(address=cls.bank_address, abi=cls.bank_abi) 44 | 45 | # Take a snapshot of this state so far 46 | cls.clean_state_snapshot = cls.eth_tester.take_snapshot() 47 | 48 | # Log contains a LogDeposit event with the following arguments 49 | # owner: self.web3.eth.accounts[0] 50 | # amount: 1 ether 51 | with open(cls.events_log_file) as log_file: 52 | test_logs = json.load(log_file) 53 | 54 | # From web3 v3 to web3 v4, the topics as we as the blockHash, transactionHash are 55 | # wrapped with the HexBytes class. 56 | cls.logs = cls._proccess_logs(test_logs) 57 | 58 | @staticmethod 59 | def _proccess_logs(test_logs): 60 | logs = deepcopy(test_logs) 61 | for index, log in enumerate(test_logs): 62 | # Convert topics to HexBytes 63 | for z, topic in enumerate(log['topics']): 64 | logs[index]['topics'][z] = HexBytes(topic) 65 | 66 | # Convert blockHash and transactionHash to HexBytes 67 | logs[index]['transactionHash'] = HexBytes(log['transactionHash']) 68 | logs[index]['blockHash'] = HexBytes(log['blockHash']) 69 | return logs 70 | 71 | def _create_deposit_event(self, event_receiver=None): 72 | if not event_receiver: 73 | event_receiver = 'django_ethereum_events.tests.test_event_listener.BankDepositEventReceiver' 74 | 75 | deposit_event = MonitoredEvent.objects.register_event( 76 | event_name='LogDeposit', 77 | contract_address=self.bank_address, 78 | contract_abi=self.bank_abi, 79 | event_receiver=event_receiver 80 | ) 81 | 82 | return deposit_event 83 | 84 | def test_monitored_event_fetched_from_backend(self): 85 | """Test that the decoder is in sync with the backend 86 | """ 87 | self._create_deposit_event() 88 | decoder = Decoder(block_number=0) 89 | 90 | self.assertEqual(len(decoder.monitored_events), 1, "LogDeposit is been monitored") 91 | 92 | def test_monitored_event_removed_from_backend(self): 93 | """Test the functionality of the refresh_state method 94 | """ 95 | self._create_deposit_event() 96 | decoder = Decoder(block_number=0) 97 | 98 | self.assertEqual(len(decoder.monitored_events), 1, "LogDeposit is been monitored") 99 | 100 | MonitoredEvent.objects.all().delete() 101 | decoder.refresh_state(block_number=1) 102 | 103 | self.assertEqual(len(decoder.monitored_events), 0, "No events to monitor") 104 | 105 | def test_decode_logs_different_address(self): 106 | """Verify that the decoder correctly decodes the test logs. 107 | """ 108 | self._create_deposit_event() 109 | decoder = Decoder(block_number=0) 110 | 111 | self.logs[0]["address"] = "0xDD474B80D5EC7F0CF986eD7FBEe2a7b4Cdc73153" 112 | decoded_logs = decoder.decode_logs(self.logs) 113 | self.assertEqual(decoded_logs, []) # empty response because wrong address 114 | 115 | def test_decode_logs(self): 116 | self._create_deposit_event() 117 | decoder = Decoder(block_number=0) 118 | 119 | # Fix logs' address 120 | self.logs[0]["address"] = self.bank_address 121 | decoded_logs = decoder.decode_logs(self.logs) 122 | 123 | self.assertEqual(len(decoded_logs), 1, "Log decoded") 124 | self.assertEqual(decoded_logs[0][1].args.amount, to_wei(1, 'ether'), "Log `amount` parameter is correct") 125 | self.assertEqual(decoded_logs[0][1].args.owner, '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1', "Log `owner` parameter is correct") 126 | -------------------------------------------------------------------------------- /django_ethereum_events/tests/test_event_listener.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import patch 3 | 4 | from django.test import TestCase 5 | from eth_tester import EthereumTester, PyEVMBackend 6 | from eth_utils import to_wei, to_bytes 7 | from web3 import EthereumTesterProvider, Web3 8 | 9 | from ..tasks import event_listener 10 | from .contracts.bank import BANK_ABI_RAW, BANK_BYTECODE 11 | from .contracts.claim import CLAIM_ABI_RAW, CLAIM_BYTECODE 12 | from ..chainevents import AbstractEventReceiver 13 | from ..event_listener import EventListener 14 | from ..models import MonitoredEvent, FailedEventLog, Daemon 15 | 16 | # Keeps track of fired events 17 | claim_events = [] 18 | bank_withdraw_events = [] 19 | bank_deposit_events = [] 20 | 21 | 22 | class ClaimEventReceiver(AbstractEventReceiver): 23 | def save(self, decoded_event): 24 | claim_events.append(decoded_event) 25 | 26 | 27 | class BankWithdrawEventReceiver(AbstractEventReceiver): 28 | def save(self, decoded_event): 29 | bank_withdraw_events.append(decoded_event) 30 | 31 | 32 | class BankDepositEventReceiver(AbstractEventReceiver): 33 | def save(self, decoded_event): 34 | bank_deposit_events.append(decoded_event) 35 | 36 | 37 | class ErroneousBankDepositEventReceiver(AbstractEventReceiver): 38 | def save(self, decoded_event): 39 | raise ValueError 40 | 41 | 42 | def patched_get_block_logs(*args, **kwargs): 43 | """Simulates a error raised by the web3 instance""" 44 | raise ValueError 45 | 46 | 47 | class EventListenerTestCase(TestCase): 48 | def setUp(self): 49 | super(EventListenerTestCase, self).setUp() 50 | 51 | def tearDown(self): 52 | super(EventListenerTestCase, self).tearDown() 53 | 54 | # Clear event receivers state 55 | claim_events.clear() 56 | bank_deposit_events.clear() 57 | bank_withdraw_events.clear() 58 | 59 | # Reset to snapshot 60 | self.eth_tester.revert_to_snapshot(self.clean_state_snapshot) 61 | 62 | @classmethod 63 | def setUpTestData(cls): 64 | cls.eth_tester = EthereumTester(backend=PyEVMBackend()) 65 | cls.provider = EthereumTesterProvider(cls.eth_tester) 66 | cls.web3 = Web3(cls.provider) 67 | 68 | # Deploy the Bank test contract 69 | cls.bank_abi = json.loads(BANK_ABI_RAW) 70 | bank_bytecode = BANK_BYTECODE 71 | 72 | Bank = cls.web3.eth.contract(abi=cls.bank_abi, bytecode=bank_bytecode) 73 | tx_hash = Bank.constructor().transact() 74 | tx_receipt = cls.web3.eth.waitForTransactionReceipt(tx_hash) 75 | cls.bank_address = tx_receipt.contractAddress 76 | cls.bank_contract = cls.web3.eth.contract(address=cls.bank_address, abi=cls.bank_abi) 77 | 78 | # Deploy the Claim test contract 79 | cls.claim_abi = json.loads(CLAIM_ABI_RAW) 80 | claim_bytecode = CLAIM_BYTECODE 81 | 82 | Claim = cls.web3.eth.contract(abi=cls.claim_abi, bytecode=claim_bytecode) 83 | tx_hash = Claim.constructor().transact() 84 | tx_receipt = cls.web3.eth.waitForTransactionReceipt(tx_hash) 85 | cls.claim_address = tx_receipt.contractAddress 86 | cls.claim_contract = cls.web3.eth.contract(address=cls.claim_address, abi=cls.claim_abi) 87 | 88 | # Take a snapshot of this state so far 89 | cls.clean_state_snapshot = cls.eth_tester.take_snapshot() 90 | 91 | def _create_deposit_event(self, event_receiver=None): 92 | if not event_receiver: 93 | event_receiver = 'django_ethereum_events.tests.test_event_listener.BankDepositEventReceiver' 94 | 95 | deposit_event = MonitoredEvent.objects.register_event( 96 | event_name='LogDeposit', 97 | contract_address=self.bank_address, 98 | contract_abi=self.bank_abi, 99 | event_receiver=event_receiver 100 | ) 101 | 102 | return deposit_event 103 | 104 | def _create_withdraw_event(self, event_receiver=None): 105 | if not event_receiver: 106 | event_receiver = 'django_ethereum_events.tests.test_event_listener.BankWithdrawEventReceiver' 107 | 108 | withdraw_event = MonitoredEvent.objects.register_event( 109 | event_name='LogWithdraw', 110 | contract_address=self.bank_address, 111 | contract_abi=self.bank_abi, 112 | event_receiver=event_receiver 113 | ) 114 | 115 | return withdraw_event 116 | 117 | def _create_claim_event(self, event_receiver=None): 118 | if not event_receiver: 119 | event_receiver = 'django_ethereum_events.tests.test_event_listener.ClaimEventReceiver' 120 | 121 | claim_event = MonitoredEvent.objects.register_event( 122 | event_name='ClaimSet', 123 | contract_address=self.claim_address, 124 | contract_abi=self.claim_abi, 125 | event_receiver=event_receiver 126 | ) 127 | 128 | return claim_event 129 | 130 | def test_pending_blocks_fetched(self): 131 | last_block_proccesed = 0 132 | blocks_to_mine = 5 133 | current = self.web3.eth.blockNumber 134 | listener = EventListener(rpc_provider=self.provider) 135 | 136 | self.assertEqual(listener.get_pending_blocks(), list(range(last_block_proccesed + 1, current + 1))) 137 | listener.execute() 138 | last_block_proccesed = current 139 | 140 | # Mine some blocks 141 | self.eth_tester.mine_blocks(num_blocks=blocks_to_mine) 142 | current = self.web3.eth.blockNumber 143 | 144 | self.assertEqual(listener.get_pending_blocks(), list(range(last_block_proccesed + 1, current + 1))) 145 | listener.execute() 146 | 147 | # All blocks processed, verify once more 148 | self.assertEqual(listener.get_pending_blocks(), []) 149 | 150 | def test_monitor_contract_single_event_once(self): 151 | """Test the monitoring of a single event fired only once. 152 | """ 153 | deposit_value = to_wei(1, 'ether') 154 | self._create_deposit_event() 155 | listener = EventListener(rpc_provider=self.provider) 156 | 157 | tx_hash = self.bank_contract.functions.deposit(). \ 158 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 159 | 160 | listener.execute() 161 | 162 | self.assertEqual(len(bank_deposit_events), 1, "Deposit event listener fired") 163 | self.assertEqual(bank_deposit_events[0].args.amount, deposit_value, "Argument fetched correctly") 164 | 165 | def test_monitor_contract_single_event_twice_same_interval(self): 166 | """Test the monitoring of a single event fired twice before the execute method was called 167 | """ 168 | 169 | deposit_value = to_wei(1, 'ether') 170 | self._create_deposit_event() 171 | listener = EventListener(rpc_provider=self.provider) 172 | 173 | tx_hash = self.bank_contract.functions.deposit(). \ 174 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 175 | 176 | tx_hash = self.bank_contract.functions.deposit(). \ 177 | transact({'from': self.web3.eth.accounts[0], 'value': 2 * deposit_value}) 178 | 179 | listener.execute() 180 | 181 | self.assertEqual(len(bank_deposit_events), 2, "Deposit event listener fired twice") 182 | self.assertEqual(bank_deposit_events[1].args.amount, 2 * deposit_value, "Argument fetched correctly the twice") 183 | 184 | def test_monitor_contract_single_event_twice_different_interval(self): 185 | """Test the monitoring of a single event fired twice in the following fashion 186 | 187 | transaction 188 | execute() 189 | transaction 190 | execute() 191 | """ 192 | 193 | deposit_value = to_wei(1, 'ether') 194 | self._create_deposit_event() 195 | listener = EventListener(rpc_provider=self.provider) 196 | 197 | tx_hash = self.bank_contract.functions.deposit(). \ 198 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 199 | 200 | listener.execute() 201 | 202 | tx_hash = self.bank_contract.functions.deposit(). \ 203 | transact({'from': self.web3.eth.accounts[0], 'value': 2 * deposit_value}) 204 | 205 | listener.execute() 206 | 207 | self.assertEqual(len(bank_deposit_events), 2, "Deposit second event fetched") 208 | self.assertEqual(bank_deposit_events[1].args.amount, 2 * deposit_value, "Argument fetched correctly") 209 | 210 | def test_monitor_contract_multiple_events(self): 211 | """Test the monitoring of multiple events in the same smart contract 212 | """ 213 | deposit_value = to_wei(1, 'ether') 214 | withdraw_value = deposit_value 215 | self._create_deposit_event() 216 | self._create_withdraw_event() 217 | 218 | listener = EventListener(rpc_provider=self.provider) 219 | 220 | tx_hash = self.bank_contract.functions.deposit(). \ 221 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 222 | 223 | tx_hash = self.bank_contract.functions.withdraw(withdraw_value). \ 224 | transact({'from': self.web3.eth.accounts[0]}) 225 | 226 | listener.execute() 227 | 228 | self.assertEqual(len(bank_deposit_events), 1, "Deposit event fired") 229 | self.assertEqual(len(bank_withdraw_events), 1, "Withdraw event fired") 230 | 231 | def test_monitor_multiple_contracts_multiple_events(self): 232 | """Test the monitoring of multiple events in multiple smart contracts 233 | """ 234 | deposit_value = to_wei(1, 'ether') 235 | withdraw_value = deposit_value 236 | key = "hello" 237 | value = "world" 238 | self._create_deposit_event() 239 | self._create_withdraw_event() 240 | self._create_claim_event() 241 | 242 | listener = EventListener(rpc_provider=self.provider) 243 | 244 | tx_hash = self.bank_contract.functions.deposit(). \ 245 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 246 | 247 | tx_hash = self.bank_contract.functions.withdraw(withdraw_value). \ 248 | transact({'from': self.web3.eth.accounts[0]}) 249 | 250 | tx_hash = self.claim_contract.functions.setClaim(to_bytes(text=key), to_bytes(text=value)). \ 251 | transact({'from': self.web3.eth.accounts[0]}) 252 | 253 | listener.execute() 254 | 255 | self.assertEqual(len(bank_deposit_events), 1, "Deposit event fired") 256 | self.assertEqual(len(bank_withdraw_events), 1, "Withdraw event fired") 257 | self.assertEqual(len(claim_events), 1, "Claim event fired") 258 | 259 | def test_monitor_event_monitored_from_value(self): 260 | """Test the added events are tracked from the next block that is ready to be processed 261 | """ 262 | current = self.web3.eth.blockNumber 263 | deposit_value = to_wei(1, 'ether') 264 | 265 | listener = EventListener(rpc_provider=self.provider) 266 | listener.execute() 267 | 268 | event = self._create_deposit_event() 269 | 270 | tx_hash = self.bank_contract.functions.deposit(). \ 271 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 272 | 273 | # intention is to the the .monitored_from field, not the cache / update mechanism 274 | # this is why we get a new instance of the event listener 275 | listener = EventListener(rpc_provider=self.provider) 276 | listener.execute() 277 | event.refresh_from_db() 278 | 279 | self.assertEqual(len(bank_deposit_events), 1, "Deposit event listener fired") 280 | self.assertEqual(event.monitored_from, current + 1) 281 | 282 | def test_monitor_event_listener_state_update_event_added(self): 283 | """Test the addition of a newly created event inside the event listener decoder state 284 | """ 285 | current = self.web3.eth.blockNumber 286 | deposit_value = to_wei(1, 'ether') 287 | 288 | listener = EventListener(rpc_provider=self.provider) 289 | listener.execute() 290 | 291 | event = self._create_deposit_event() # this will fire a signal which will update a cache value 292 | 293 | tx_hash = self.bank_contract.functions.deposit(). \ 294 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 295 | 296 | listener.execute() # updated cache value from previous line will force data to be re-fetched from the backend 297 | event.refresh_from_db() 298 | 299 | self.assertEqual(len(bank_deposit_events), 1, "Deposit event listener fired") 300 | self.assertEqual(event.monitored_from, current + 1) 301 | 302 | def test_erroneous_event_receiver_impl(self): 303 | self._create_deposit_event( 304 | event_receiver='django_ethereum_events.tests.test_event_listener.ErroneousBankDepositEventReceiver') 305 | 306 | deposit_value = to_wei(1, 'ether') 307 | 308 | tx_hash = self.bank_contract.functions.deposit(). \ 309 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 310 | 311 | listener = EventListener(rpc_provider=self.provider) 312 | listener.execute() 313 | 314 | failed_events = FailedEventLog.objects.all() 315 | self.assertEqual(listener.daemon.block_number, self.web3.eth.blockNumber, 316 | "Exception did not cause listener to stop") 317 | self.assertEqual(failed_events.count(), 1, "Failed event log created") 318 | stored_args = json.loads(failed_events.first().args) 319 | self.assertEqual(stored_args['amount'], deposit_value, "Failed event log saved correct arguments") 320 | 321 | def test_event_listener_task(self): 322 | """Test that the event listener task is working as intended""" 323 | deposit_value = to_wei(1, 'ether') 324 | event = self._create_deposit_event() 325 | 326 | tx_hash = self.bank_contract.functions.deposit(). \ 327 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 328 | 329 | current = self.web3.eth.blockNumber 330 | event_listener() 331 | 332 | daemon = Daemon.get_solo() 333 | self.assertEqual(daemon.block_number, current, 'Task run successfully') 334 | self.assertEqual(daemon.last_error_block_number, 0, 'No errors during task execution') 335 | 336 | def test_event_listener_task_exception_raised(self): 337 | """Determine if the event listener task correctly sets the daemon.last_error_block_number 338 | when an exception occurs that is unhandled. 339 | 340 | """ 341 | current = self.web3.eth.blockNumber 342 | event_listener() 343 | daemon = Daemon.get_solo() 344 | 345 | self.assertEqual(daemon.block_number, current, 'Task run successfully') 346 | self.assertEqual(daemon.last_error_block_number, 0, 'No errors during task execution') 347 | 348 | # Create a new monitored event, run the task again but with the `get_block_logs` method patched 349 | # to simulate an error raised from the web3 instance (e.g. rcp node is down) 350 | deposit_value = to_wei(1, 'ether') 351 | event = self._create_deposit_event() 352 | 353 | tx_hash = self.bank_contract.functions.deposit(). \ 354 | transact({'from': self.web3.eth.accounts[0], 'value': deposit_value}) 355 | 356 | with patch.object(EventListener, 'get_block_logs', patched_get_block_logs): 357 | event_listener() 358 | 359 | daemon.refresh_from_db() 360 | self.assertEqual(daemon.block_number, current, 'Erroneous block was not processed') 361 | self.assertEqual(daemon.last_error_block_number, current + 1, 'Error block was updated') 362 | -------------------------------------------------------------------------------- /django_ethereum_events/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.cache import cache 4 | 5 | from eth_utils import encode_hex, event_abi_to_log_topic 6 | 7 | from hexbytes import HexBytes 8 | 9 | from web3.datastructures import AttributeDict 10 | 11 | 12 | def get_event_abi(abi, event_name): 13 | """Helper function that extracts the event abi from the given abi. 14 | 15 | Args: 16 | abi (list): the contract abi 17 | event_name (str): the event name 18 | 19 | Returns: 20 | dict: the event specific abi 21 | """ 22 | for entry in abi: 23 | if 'name' in entry.keys() and entry['name'] == event_name and \ 24 | entry['type'] == "event": 25 | return entry 26 | raise ValueError( 27 | 'Event `{0}` not found in the contract abi'.format(event_name)) 28 | 29 | 30 | def event_topic_from_contract_abi(abi, event_name): 31 | """Returns the event topic from the contract abi. 32 | 33 | Args: 34 | abi (obj): contract abi 35 | event_name (str): the desired event 36 | 37 | Returns: 38 | the event topic in hexstring form 39 | """ 40 | if isinstance(abi, str): 41 | abi = json.loads(abi) 42 | 43 | event_abi = get_event_abi(abi, event_name) 44 | event_topic = event_abi_to_log_topic(event_abi) 45 | return event_topic.hex() 46 | 47 | 48 | def refresh_cache_update_value(update_required=False): 49 | from .models import CACHE_UPDATE_KEY 50 | cache.set(CACHE_UPDATE_KEY, update_required) 51 | 52 | 53 | class Singleton(type): 54 | """Simple singleton implementation.""" 55 | 56 | _instances = {} 57 | 58 | def __call__(cls, *args, **kwargs): # noqa: N805 59 | if cls not in cls._instances: 60 | cls._instances[cls] = super( 61 | Singleton, cls).__call__(*args, **kwargs) 62 | return cls._instances[cls] 63 | 64 | 65 | class HexJsonEncoder(json.JSONEncoder): 66 | """JSONEncoder that parses decoded logs as returned from `web3.utils.events.get_event_data`.""" 67 | 68 | def default(self, obj): 69 | if isinstance(obj, HexBytes): 70 | return obj.hex() 71 | elif isinstance(obj, AttributeDict): 72 | return dict(obj) 73 | elif isinstance(obj, bytes): 74 | return encode_hex(obj) 75 | return super().default(obj) 76 | -------------------------------------------------------------------------------- /django_ethereum_events/web3_service.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from web3 import HTTPProvider, Web3 4 | from web3.middleware import geth_poa_middleware 5 | 6 | from .utils import Singleton 7 | 8 | 9 | class Web3Service(metaclass=Singleton): 10 | """Creates a `web3` instance based on the given Provider.""" 11 | 12 | def __init__(self, *args, **kwargs): 13 | """Initializes the `web3` object. 14 | 15 | Args: 16 | rpc_provider (HTTPProvider): Valid `web3` HTTPProvider instance (optional) 17 | """ 18 | rpc_provider = kwargs.pop('rpc_provider', None) 19 | if not rpc_provider: 20 | timeout = getattr(settings, "ETHEREUM_NODE_TIMEOUT", 10) 21 | 22 | uri = settings.ETHEREUM_NODE_URI 23 | rpc_provider = HTTPProvider( 24 | endpoint_uri=uri, 25 | request_kwargs={ 26 | "timeout": timeout 27 | } 28 | ) 29 | 30 | self.web3 = Web3(rpc_provider) 31 | 32 | # If running in a network with PoA consensus, inject the middleware 33 | if getattr(settings, "ETHEREUM_GETH_POA", False): 34 | self.web3.middleware_onion.inject(geth_poa_middleware, layer=0) 35 | 36 | super(Web3Service, self).__init__() 37 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Here is an example django app used for developing using a smart contract defined in ``contracts`` folder. 2 | 3 | ## Local usage 4 | In one terminal, run the dev blockchain: 5 | 6 | ```bash 7 | cd contracts 8 | npm install 9 | npm run dev-blockchain 10 | ``` 11 | 12 | In a second terminal, deploy the smart contract 13 | ```bash 14 | cd contracts 15 | npm run deploy-local 16 | ``` 17 | 18 | Install local package 19 | 20 | ```bash 21 | pip3 install -e ../ 22 | ``` 23 | 24 | Make migrations 25 | 26 | ```bash 27 | python3 manage.py migrate 28 | ``` 29 | 30 | Register smart contract event(s) 31 | 32 | ```bash 33 | python3 manage.py register_events 34 | ``` 35 | 36 | Send an `echo` transaction 37 | 38 | ```bash 39 | python3 manage.py send_echo 40 | ``` 41 | 42 | Invoke the event listener, check that event was captured 43 | ```bash 44 | python3 manage.py run_listener 45 | ``` 46 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/example/__init__.py -------------------------------------------------------------------------------- /example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /example/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DevConfig(AppConfig): 5 | name = 'dev' 6 | -------------------------------------------------------------------------------- /example/contracts/README.md: -------------------------------------------------------------------------------- 1 | # Example echo dapp 2 | 3 | The following project holds an example DAPP to be used for dev purposes. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | ## Compiling the smart contracts 12 | 13 | ```bash 14 | npm run compile 15 | ``` 16 | 17 | ## Development 18 | In one terminal execute: 19 | 20 | ```bash 21 | npm run dev-blockchain 22 | ``` 23 | 24 | and in the other 25 | 26 | ```bash 27 | npm run deploy-local 28 | ``` 29 | 30 | ## Unit tests 31 | 32 | ```bash 33 | npm run test 34 | ``` 35 | -------------------------------------------------------------------------------- /example/contracts/contracts/Echo.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.22; 2 | 3 | contract Echo { 4 | event LogEcho(string message, address sender, uint timestamp); 5 | 6 | function echo(string message) public { 7 | emit LogEcho(message, msg.sender, now); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/contracts/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/contracts/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const TruffleConfig = require('../truffle-config') 3 | const Migrations = artifacts.require("Migrations"); 4 | 5 | 6 | module.exports = function(deployer, network, addresses) { 7 | const config = TruffleConfig.networks[network]; 8 | 9 | if(process.env.ACCOUNT_PASSWORD) { 10 | // Unlock account before making deployment 11 | const web3 = new Web3(new Web3.providers.HttpProvider('http://' + config.host + ':' + config.port)); 12 | 13 | console.log('Unlocking account ' + config.from); 14 | web3.eth.personal.unlockAccount(config.from, process.env.ACCOUNT_PASSWORD, 36000); 15 | 16 | } 17 | 18 | deployer.deploy(Migrations); 19 | }; 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/contracts/migrations/2_deploy_echo.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const TruffleConfig = require('../truffle-config') 3 | let Echo = artifacts.require('./Echo.sol') 4 | 5 | module.exports = function(deployer, network, addresses) { 6 | const config = TruffleConfig.networks[network]; 7 | 8 | if(process.env.ACCOUNT_PASSWORD) { 9 | // Unlock account before making deployment 10 | const web3 = new Web3(new Web3.providers.HttpProvider('http://' + config.host + ':' + config.port)); 11 | 12 | console.log('Unlocking account ' + config.from); 13 | web3.eth.personal.unlockAccount(config.from, process.env.ACCOUNT_PASSWORD, 36000); 14 | 15 | } 16 | 17 | deployer.deploy(Echo); 18 | }; 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo-app", 3 | "version": "1.0.0", 4 | "description": "Example echo app", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/truffle test", 8 | "compile": "./node_modules/.bin/truffle compile", 9 | "dev-blockchain": "./node_modules/.bin/ganache-cli -d --db chain/", 10 | "deploy-local": "./node_modules/.bin/truffle migrate --network local" 11 | }, 12 | "author": { 13 | "name": "Artemios Tomaras", 14 | "email": "artemistomaras@gmail.com", 15 | "url": "https://github.com/artemistomaras/" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "chai": "^4.2.0", 20 | "ganache-cli": "^6.4.3", 21 | "keythereum": "^1.0.4", 22 | "truffle": "^5.0.19", 23 | "truffle-assertions": "^0.9.2", 24 | "truffle-hdwallet-provider": "^1.0.17", 25 | "web3": "^1.2.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/contracts/test/echo.js: -------------------------------------------------------------------------------- 1 | const truffleAssert = require('truffle-assertions'); 2 | const Echo = artifacts.require('Echo'); 3 | 4 | contract('Echo', accounts => { 5 | let echoContract; 6 | 7 | it('should be able to echo a message', async () => { 8 | let message = "hello world!" 9 | let tx = await echoContract.echo(accounts[2], details, timestamp, {from: owner}) 10 | 11 | truffleAssert.eventEmitted(tx, 'LogEcho', (ev) => { 12 | return ev.message == message; 13 | }); 14 | }); 15 | 16 | beforeEach(async () => { 17 | echoContract = await Echo.new({from: accounts[0]}) 18 | }) 19 | 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /example/contracts/truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('truffle-hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | local: { 40 | host: "127.0.0.1", // Localhost (default: none) 41 | port: 8545, // Standard Ethereum port (default: none) 42 | network_id: "*", // Any network (default: none) 43 | }, 44 | }, 45 | 46 | // Set default mocha options here, use special reporters etc. 47 | mocha: { 48 | // timeout: 100000 49 | }, 50 | 51 | // Configure your compilers 52 | compilers: { 53 | solc: { 54 | version: "0.4.22", // Fetch exact version from solc-bin (default: truffle's version) 55 | docker: false, // Use "0.5.1" you've installed locally with docker (default: false) 56 | settings: { // See the solidity docs for advice about optimization and evmVersion 57 | optimizer: { 58 | enabled: false, 59 | runs: 200 60 | }, 61 | evmVersion: "byzantium" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/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", "settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /example/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/example/management/__init__.py -------------------------------------------------------------------------------- /example/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/example/management/commands/__init__.py -------------------------------------------------------------------------------- /example/management/commands/clean_state.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from django_ethereum_events.models import Daemon, MonitoredEvent, FailedEventLog 3 | 4 | 5 | 6 | class Command(BaseCommand): 7 | """Use only for development! 8 | """ 9 | help = 'Cleans the app state (only for dev!)' 10 | 11 | def handle(self, *args, **options): 12 | Daemon.get_solo().delete() 13 | self.stdout.write(self.style.SUCCESS('Reset block monitoring to 0...')) 14 | 15 | MonitoredEvent.objects.all().update(monitored_from=None) 16 | FailedEventLog.objects.all().delete() 17 | self.stdout.write(self.style.SUCCESS('Deleted failed event logs...')) -------------------------------------------------------------------------------- /example/management/commands/register_events.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.core.management import BaseCommand 4 | 5 | from django_ethereum_events.chainevents import AbstractEventReceiver 6 | from django_ethereum_events.models import MonitoredEvent 7 | 8 | echo_address = "0xCfEB869F69431e42cdB54A4F4f105C19C080A601" 9 | echo_abi = json.loads(""" 10 | [ 11 | { 12 | "anonymous": false, 13 | "inputs": [ 14 | { 15 | "indexed": false, 16 | "name": "message", 17 | "type": "string" 18 | }, 19 | { 20 | "indexed": false, 21 | "name": "sender", 22 | "type": "address" 23 | }, 24 | { 25 | "indexed": false, 26 | "name": "timestamp", 27 | "type": "uint256" 28 | } 29 | ], 30 | "name": "LogEcho", 31 | "type": "event" 32 | }, 33 | { 34 | "constant": false, 35 | "inputs": [ 36 | { 37 | "name": "message", 38 | "type": "string" 39 | } 40 | ], 41 | "name": "echo", 42 | "outputs": [], 43 | "payable": false, 44 | "stateMutability": "nonpayable", 45 | "type": "function" 46 | } 47 | ] 48 | """) 49 | 50 | 51 | class TestReceiver(AbstractEventReceiver): 52 | 53 | def save(self, decoded_event): 54 | print('Received event: {}'.format(decoded_event)) 55 | 56 | 57 | receiver = 'example.management.commands.register_events.TestReceiver' 58 | 59 | # List of ethereum events to monitor the blockchain for 60 | DEFAULT_EVENTS = [ 61 | ('LogEcho', echo_address, echo_abi, receiver), 62 | ] 63 | 64 | 65 | class Command(BaseCommand): 66 | def handle(self, *args, **options): 67 | monitored_events = MonitoredEvent.objects.all() 68 | for event in DEFAULT_EVENTS: 69 | 70 | if not monitored_events.filter(name=event[0], contract_address__iexact=event[1]).exists(): 71 | self.stdout.write('Creating monitor for event {} at {}'.format(event[0], event[1])) 72 | 73 | MonitoredEvent.objects.register_event(*event) 74 | 75 | self.stdout.write(self.style.SUCCESS('Events are up to date')) 76 | -------------------------------------------------------------------------------- /example/management/commands/run_listener.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | 3 | from django_ethereum_events.tasks import event_listener 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | print('Running listener...') 9 | event_listener() 10 | print('Listener terminated...') 11 | -------------------------------------------------------------------------------- /example/management/commands/send_echo.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management import BaseCommand 3 | from web3 import Web3, HTTPProvider 4 | 5 | from example.management.commands.register_events import echo_address, echo_abi 6 | 7 | 8 | class Command(BaseCommand): 9 | def handle(self, *args, **options): 10 | web3 = Web3(HTTPProvider('http://localhost:8545')) 11 | echo_contract = web3.eth.contract(echo_address, abi=echo_abi) 12 | txn_hash = echo_contract.functions.echo("hello").transact({'from': settings.WALLET_ADDRESS}) 13 | print('Received txn_hash={} ...'.format(txn_hash)) 14 | print('Waiting for transaction receipt...') 15 | txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash) 16 | print('Received transaction receipt: {}'.format(txn_receipt)) 17 | -------------------------------------------------------------------------------- /example/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemistomaras/django-ethereum-events/46cc4cc8618fdcf8beb68275ef208ac049676db5/example/migrations/__init__.py -------------------------------------------------------------------------------- /example/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | from os import pardir, path 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | from celery.schedules import crontab 5 | 6 | BASE_DIR = path.dirname( 7 | path.abspath(path.join(__file__)) 8 | ) 9 | 10 | # SECURITY WARNING: keep the secret key used in production secret! 11 | SECRET_KEY = 'wm82)$7ay(ya#g788(4s#hq1w2cynlt$+2ay_noapdj-#6h7xl' 12 | 13 | # SECURITY WARNING: don't run with debug turned on in production! 14 | DEBUG = True 15 | 16 | INSTALLED_APPS = ( 17 | 'django.contrib.admin', 18 | 'django.contrib.auth', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.sessions', 21 | 'django.contrib.messages', 22 | 'django.contrib.staticfiles', 23 | 24 | 'django_ethereum_events', 25 | 'solo', 26 | 'example' 27 | ) 28 | 29 | CACHES = { 30 | 'default': { 31 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 32 | } 33 | } 34 | 35 | DATABASES = { 36 | 'default': { 37 | 'ENGINE': 'django.db.backends.sqlite3', 38 | 'NAME': path.join(BASE_DIR, 'db.sqlite3'), 39 | } 40 | } 41 | 42 | CELERY_ALWAYS_EAGER = True 43 | 44 | ROOT_URLCONF = 'example.urls' 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [] 60 | , 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | LANGUAGE_CODE = 'en-us' 74 | TIME_ZONE = 'UTC' 75 | USE_I18N = True 76 | USE_L10N = True 77 | USE_TZ = True 78 | 79 | # Static files (CSS, JavaScript, Images) 80 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 81 | STATIC_ROOT = path.join(BASE_DIR, 'static') 82 | STATIC_URL = '/static/' 83 | 84 | ETHEREUM_NODE_URI = 'http://localhost:8545' 85 | ETHEREUM_NODE_SSL = False 86 | WALLET_ADDRESS = '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1' 87 | -------------------------------------------------------------------------------- /example/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | ] 7 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | 7 | 8 | def runtests(): 9 | settings_file = 'django_ethereum_events.settings.test' 10 | if not settings.configured: 11 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_file) 12 | 13 | django.setup() 14 | 15 | from django.test.runner import DiscoverRunner 16 | runner_class = DiscoverRunner 17 | test_args = ['django_ethereum_events'] 18 | 19 | failures = runner_class( 20 | verbosity=1, interactive=True, failfast=False).run_tests(test_args) 21 | sys.exit(failures) 22 | 23 | 24 | if __name__ == '__main__': 25 | runtests() 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 6 | README = readme.read() 7 | 8 | # allow setup.py to be run from any path 9 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 10 | 11 | extras_require = { 12 | 'tester': [ 13 | 'eth-tester[py-evm]==v0.2.0-beta.2' 14 | ], 15 | 'dev': [ 16 | 'tox>=1.8.0' 17 | 'twine>=1.13,<2' 18 | 'wheel' 19 | ], 20 | 'linter': [ 21 | 'flake8==3.7.9' 22 | ] 23 | } 24 | 25 | extras_require['dev'] = ( 26 | extras_require['tester'], 27 | extras_require['dev'], 28 | extras_require['linter'] 29 | ) 30 | 31 | setup( 32 | name='django-ethereum-events', 33 | version='4.2.0', 34 | packages=find_packages(exclude=['example']), 35 | include_package_data=True, 36 | install_requires=[ 37 | 'Django>=1.11', 38 | 'celery>=3.1.25', 39 | 'django-solo>=1.1.0', 40 | 'web3>=5.5.0,<6', 41 | ], 42 | extras_require=extras_require, 43 | python_requires='>=3.6,<4', 44 | license='MIT License', 45 | description='Django Ethereum Events', 46 | long_description=README, 47 | url='https://github.com/artemistomaras/django-ethereum-events', 48 | author='Artemios Tomaras', 49 | author_email='artemistomaras@gmail.com', 50 | keywords='django ethereum', 51 | classifiers=[ 52 | 'Environment :: Web Environment', 53 | 'Framework :: Django', 54 | 'Framework :: Django :: 1.11', 55 | 'Framework :: Django :: 2.0', 56 | 'Framework :: Django :: 2.1', 57 | 'Framework :: Django :: 2.2', 58 | 'Framework :: Django :: 3.0', 59 | 'Framework :: Django :: 3.1', 60 | 'Intended Audience :: Developers', 61 | 'License :: OSI Approved :: MIT License', 62 | 'Operating System :: OS Independent', 63 | 'Programming Language :: Python', 64 | 'Programming Language :: Python :: 3.6', 65 | 'Programming Language :: Python :: 3.7', 66 | 'Topic :: Internet :: WWW/HTTP', 67 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{36,37}-django{111,20,21,22,30,31}-webthree{55} 4 | flake8 5 | 6 | [testenv] 7 | passenv = CI TRAVIS TRAVIS_* 8 | deps = 9 | .[dev] 10 | coverage 11 | django111: Django>=1.11,<1.12 12 | django20: Django>=2.0,<2.1 13 | django21: Django>=2.1,<2.2 14 | django22: Django>=2.2,<2.3 15 | django30: Django>=3.0,<3.1 16 | django31: Django>=3.1,<3.2 17 | webthree55: web3>=5.5.0,<6 18 | commands = 19 | coverage run runtests.py 20 | coverage report 21 | 22 | [testenv:flake8] 23 | deps = 24 | .[linter] 25 | commands = flake8 {toxinidir}/django_ethereum_events 26 | 27 | [travis] 28 | python: 29 | 3.6: py36, flake8 30 | 3.7: py37 31 | 32 | [travis:env] 33 | DJANGO = 34 | 1.11: django111 35 | 2.0: django20 36 | 2.1: django21 37 | 2.2: django22 38 | 3.0: django30 39 | 3.1: django31 40 | WEBTHREE= 41 | 5.5: webthree55 42 | --------------------------------------------------------------------------------