Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event subscribing, listening, retrieving #1453

Merged
merged 40 commits into from
May 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fc55a37
Not tested, event subscriber
YerimB Jan 27, 2022
3aa186b
Fix ContractEvent related tests
YerimB Feb 2, 2022
82ff78b
Doc
YerimB Feb 3, 2022
47c13d7
starting to create tests
YerimB Feb 3, 2022
1c2ee68
Test event subscriber
YerimB Feb 4, 2022
80f8761
Update event.py
YerimB Feb 4, 2022
33adaa3
tox -e linting
YerimB Feb 4, 2022
2eb22c0
Documentation
YerimB Feb 4, 2022
532a362
Documentation
YerimB Feb 6, 2022
27fb034
Update .gitignore
YerimB Feb 6, 2022
01955d3
Merge branch 'eth-brownie:master' into events_1
YerimB Feb 14, 2022
3bd5d8f
Update CHANGELOG.md
YerimB Feb 14, 2022
88980b9
Merge branch 'master' into events_1
YerimB Feb 15, 2022
87c23f5
Define type on variable
YerimB Feb 18, 2022
20e9cb7
Merge branch 'events_1' of https://github.com/YerimB/brownie into eve…
YerimB Feb 18, 2022
c9ac9a1
Update test_event.py
YerimB Feb 18, 2022
1972416
Code reducing
YerimB Feb 18, 2022
034c315
Update contract.py
YerimB Feb 18, 2022
d074066
Missing argument bugfix
YerimB Feb 18, 2022
4ff9846
Removing useless argument from function call
YerimB Feb 20, 2022
ff54863
Thread limited event watcher
YerimB Feb 21, 2022
c22df7c
Multithreading issues fix 1
YerimB Feb 22, 2022
1e75ae8
Move recently created classes to a more meaningful file
YerimB Feb 22, 2022
838febe
Added a way to listen for an event
YerimB Feb 22, 2022
254bd22
Updating classes and fixing first tests
YerimB Feb 23, 2022
0f1e285
EventWatchData class update, Comments & Bugfix
YerimB Feb 23, 2022
1048886
ThreadPool implementation to execute callbacks
YerimB Feb 23, 2022
cce184c
Creating thread on callback instruction
YerimB Feb 24, 2022
735f42b
Renaming, Comments and attempt to join thread on stop instruction (to…
YerimB Feb 24, 2022
e9d95a7
Refactor Contract.events.listen function & Comments
YerimB Feb 24, 2022
b38c37f
Skip personal test
YerimB Feb 24, 2022
454541e
Adding 2 event trigger tests
YerimB Feb 24, 2022
4e5b68a
Start updating documentation
YerimB Feb 25, 2022
ae08b32
Renamed EventWatchData and Updated event.py
YerimB Feb 25, 2022
9b5a60a
Remove useless comments & update docs
YerimB Feb 25, 2022
6053b99
Rectified comments within event.py
YerimB Feb 25, 2022
c5c7987
Fix `tox -e lint`
YerimB Feb 25, 2022
43f3986
Fix `tox -e lint`
YerimB Feb 25, 2022
a898591
Fix `tox -e lint`
YerimB Feb 25, 2022
74a8486
Update CHANGELOG.md
YerimB Feb 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ build/
coverage.xml
dist/
docs/_build
venv/
venv*/
env/
*egg-info/
.tox/
.mypy_cache/
.idea/
.DS_Store
.hypothesis/

# Personnal
.env
c_pytest.py
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This changelog format is based on [Keep a Changelog](https://keepachangelog.com/
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/eth-brownie/brownie)
### Added
- Allow to subscribe to events, with callbacks, using the corresponding contract instance.
- Allow to retrieve events that have occurred between two blocks using the corresponding contract instance.
- Allow listening for an event using the associated contract instance.

## [1.18.1](https://github.com/eth-brownie/brownie/tree/v1.18.1) - 2022-02-15
### Fixed
Expand Down
2 changes: 1 addition & 1 deletion brownie/network/alert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/python3

import time as time
import time
from threading import Thread
from typing import Callable, Dict, List, Tuple, Union

Expand Down
158 changes: 154 additions & 4 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/python3

import asyncio
import io
import json
import os
Expand All @@ -9,17 +10,32 @@
from pathlib import Path
from textwrap import TextWrapper
from threading import get_ident # noqa
from typing import Any, Dict, Iterator, List, Match, Optional, Set, Tuple, Union
from typing import (
Any,
Callable,
Coroutine,
Dict,
Iterator,
List,
Match,
Optional,
Set,
Tuple,
Union,
)
from urllib.parse import urlparse

import eth_abi
import requests
import solcx
from eth_utils import remove_0x_prefix
from eth_utils import combomethod, remove_0x_prefix
from hexbytes import HexBytes
from semantic_version import Version
from vvm import get_installable_vyper_versions
from vvm.utils.convert import to_vyper_version
from web3._utils import filters
from web3.datastructures import AttributeDict
from web3.types import LogReceipt

from brownie._config import BROWNIE_FOLDER, CONFIG, REQUEST_HEADERS, _load_project_compiler_config
from brownie.convert.datatypes import Wei
Expand All @@ -44,7 +60,7 @@
from brownie.utils import color

from . import accounts, chain
from .event import _add_deployment_topics, _get_topics
from .event import _add_deployment_topics, _get_topics, event_watcher
from .state import (
_add_contract,
_add_deployment,
Expand All @@ -54,7 +70,7 @@
_remove_deployment,
_revert_register,
)
from .web3 import _resolve_address, web3
from .web3 import ContractEvent, _ContractEvents, _resolve_address, web3

_unverified_addresses: Set = set()

Expand Down Expand Up @@ -692,6 +708,7 @@ def __init__(
self._owner = owner
self.tx = tx
self.address = address
self.events = ContractEvents(self)
_add_deployment_topics(address, self.abi)

fn_names = [i["name"] for i in self.abi if i["type"] == "function"]
Expand Down Expand Up @@ -1287,6 +1304,139 @@ def __init__(
_DeployedContractBase.__init__(self, address, owner, tx)


class ContractEvents(_ContractEvents):
def __init__(self, contract: _DeployedContractBase):
self.linked_contract = contract

# Ignoring type since ChecksumAddress type is an alias for string
_ContractEvents.__init__(self, contract.abi, web3, contract.address) # type: ignore

def subscribe(
self, event_name: str, callback: Callable[[AttributeDict], None], delay: float = 2.0
) -> None:
"""
Subscribe to event with a name matching 'event_name', calling the 'callback'
function on new occurence giving as parameter the event log receipt.

Args:
event_name (str): Name of the event to subscribe to.
callback (Callable[[AttributeDict], None]): Function called whenever an event occurs.
delay (float, optional): Delay between each check for new events. Defaults to 2.0.
"""
target_event: ContractEvent = self.__getitem__(event_name) # type: ignore
event_watcher.add_event_callback(event=target_event, callback=callback, delay=delay)

def get_sequence(
self, from_block: int, to_block: int = None, event_type: Union[ContractEvent, str] = None
) -> Union[List[AttributeDict], AttributeDict]:
"""Returns the logs of events of type 'event_type' that occurred between the
blocks 'from_block' and 'to_block'. If 'event_type' is not specified,
it retrieves the occurrences of all events in the contract.

Args:
from_block (int): The block from which to search for events that have occurred.
to_block (int, optional): The block on which to stop searching for events.
if not specified, it is set to the most recently mined block (web3.eth.block_number).
Defaults to None.
event_type (ContractEvent, str, optional): Type or name of the event to be searched
between the specified blocks. Defaults to None.

Returns:
if 'event_type' is specified:
[list]: List of events of type 'event_type' that occured between
'from_block' and 'to_block'.
else:
event_logbook [dict]: Dictionnary of events of the contract that occured
between 'from_block' and 'to_block'.
"""
if to_block is None or to_block > web3.eth.block_number:
to_block = web3.eth.block_number

# Returns event sequence for the specified event
if event_type is not None:
if isinstance(event_type, str):
# If 'event_type' is a string, search for an event with a name matching it.
event_type: ContractEvent = self.__getitem__(event_type) # type: ignore
return self._retrieve_contract_events(event_type, from_block, to_block)

# Returns event sequence for all contract events
events_logbook = dict()
for event in ContractEvents.__iter__(self):
events_logbook[event.event_name] = self._retrieve_contract_events(
event, from_block, to_block
)
return AttributeDict(events_logbook)

def listen(self, event_name: str, timeout: float = 0) -> Coroutine:
"""
Creates a listening Coroutine object ending whenever an event matching
'event_name' occurs. If timeout is superior to zero and no event matching
'event_name' has occured, the Coroutine ends when the timeout is reached.

The Coroutine return value is an AttributeDict filled with the following fileds :
- 'event_data' (AttributeDict): The event log receipt that was caught.
- 'timed_out' (bool): False if the event did not timeout, else True

If the 'timeout' parameter is not passed or is inferior or equal to 0,
the Coroutine listens indefinitely.

Args:
event_name (str): Name of the event to be listened to.
timeout (float, optional): Timeout value in seconds. Defaults to 0.

Returns:
Coroutine: Awaitable object listening for the event matching 'event_name'.
"""
_triggered: bool = False
_received_data: Union[AttributeDict, None] = None

def _event_callback(event_data: AttributeDict) -> None:
"""
Fills the nonlocal varialbe '_received_data' with the received
argument 'event_data' and sets the nonlocal '_triggered' variable to True
"""
nonlocal _triggered, _received_data
_received_data = event_data
_triggered = True

_listener_end_time = time.time() + timeout

async def _listening_task(is_timeout: bool, end_time: float) -> AttributeDict:
"""Generates and returns a coroutine listening for an event"""
nonlocal _triggered, _received_data
timed_out: bool = False

while not _triggered:
if is_timeout and end_time <= time.time():
timed_out = True
break
await asyncio.sleep(0.05)
return AttributeDict({"event_data": _received_data, "timed_out": timed_out})

target_event: ContractEvent = self.__getitem__(event_name) # type: ignore
event_watcher.add_event_callback(
event=target_event, callback=_event_callback, delay=0.2, repeat=False
)
return _listening_task(bool(timeout > 0), _listener_end_time)

@combomethod
def _retrieve_contract_events(
self, event_type: ContractEvent, from_block: int = None, to_block: int = None
) -> List[LogReceipt]:
"""
Retrieves all log receipts from 'event_type' between 'from_block' and 'to_block' blocks
"""
if to_block is None:
to_block = web3.eth.block_number
if from_block is None and isinstance(to_block, int):
from_block = to_block - 10

event_filter: filters.LogFilter = event_type.createFilter(
fromBlock=from_block, toBlock=to_block
)
return event_filter.get_all_entries()


class OverloadedMethod:
def __init__(self, address: str, name: str, owner: Optional[AccountsType]):
self._address = address
Expand Down
Loading