def get_event_abi(self, contract_name: str, event_name: str) -> ABIEvent: """Returns the ABI for a given event.""" # Import locally to avoid web3 dependency during installation via `compile_contracts` from web3._utils.contracts import find_matching_event_abi assert self.contracts, "ContractManager should have contracts compiled" contract_abi = self.get_contract_abi(contract_name) return find_matching_event_abi(abi=contract_abi, event_name=event_name)
def get_event_logs_from_tx_receipt(instance: Any, event_name: str, tx_receipt: Any) -> Iterator[Any]: """ Query a transaction receipt for all events emitted by the given contract instance with a given event name. Yields an iterator of event-specific objects to be decoded by the caller. This function intentionally avoids connecting to a node, or creating host-side filters. """ contract_address = instance.address contract_event = instance.events[event_name]() event_abi = find_matching_event_abi(instance.abi, event_name=event_name) log_topic = event_abi_to_log_topic(cast(Dict[str, Any], event_abi)) for log in tx_receipt.logs: if log.address == contract_address and log_topic == log['topics'][0]: yield contract_event.processLog(log)
def get_event_logs(web3: Any, instance: Any, event_name: str, start_block: int, end_block: int, batch_size: Optional[int]) -> Iterator[Any]: """ Query the attached node for all events emitted by the given contract instance, with the given name. Yields an iterator of event-specific objects to be decoded by the caller. """ # It is possible to achieve this via the contract interface, with code of # the form: # # event = instance.events[event_name] # filter = event.createFilter(fromBlock=start_block, toBlock=to_block) # logs = web3.eth.getFilterLogs(filter) # # However, this creates filters on the host node, which may not be # permitted in all configurations. Hence, the code here iterates manually, # skpping events with other topics, from the same contract. contract_address = instance.address contract_event = instance.events[event_name]() event_abi = find_matching_event_abi(instance.abi, event_name=event_name) log_topic = event_abi_to_log_topic(cast(Dict[str, Any], event_abi)) batch_size = batch_size or SYNC_BLOCKS_PER_BATCH while start_block <= end_block: # Filters are *inclusive* wrt "toBlock", hence the -1 here, and +1 to # set start_block before iterating. to_block = min(start_block + batch_size - 1, end_block) filter_params = { 'fromBlock': start_block, 'toBlock': to_block, 'address': contract_address, } logs = web3.eth.getLogs(filter_params) for log in logs: if log_topic == log['topics'][0]: yield contract_event.processLog(log) start_block = to_block + 1
def _get_logs( self, web3: Optional[Web3], contract_address: ChecksumEthAddress, abi: List, event_name: str, argument_filters: Dict[str, Any], from_block: int, to_block: Union[int, Literal['latest']] = 'latest', ) -> List[Dict[str, Any]]: """Queries logs of an ethereum contract May raise: - RemoteError if etherscan is used and there is a problem with reaching it or with the returned result """ event_abi = find_matching_event_abi(abi=abi, event_name=event_name) _, filter_args = construct_event_filter_params( event_abi=event_abi, abi_codec=Web3().codec, contract_address=contract_address, argument_filters=argument_filters, fromBlock=from_block, toBlock=to_block, ) if event_abi['anonymous']: # web3.py does not handle the anonymous events correctly and adds the first topic filter_args['topics'] = filter_args['topics'][1:] events: List[Dict[str, Any]] = [] start_block = from_block if web3 is not None: events = _query_web3_get_logs( web3=web3, filter_args=filter_args, from_block=from_block, to_block=to_block, contract_address=contract_address, event_name=event_name, argument_filters=argument_filters, ) else: # etherscan until_block = (self.etherscan.get_latest_block_number() if to_block == 'latest' else to_block) blocks_step = 300000 while start_block <= until_block: while True: # loop to continuously reduce block range if need b end_block = min(start_block + blocks_step, until_block) try: new_events = self.etherscan.get_logs( contract_address=contract_address, topics=filter_args['topics'], # type: ignore from_block=start_block, to_block=end_block, ) except RemoteError as e: if 'Please select a smaller result dataset' in str(e): blocks_step = blocks_step // 2 if blocks_step < 100: raise # stop trying # else try with the smaller step continue # else some other error raise break # we must have a result # Turn all Hex ints to ints for e_idx, event in enumerate(new_events): try: block_number = deserialize_int_from_hex( symbol=event['blockNumber'], location='etherscan log query', ) log_index = deserialize_int_from_hex( symbol=event['logIndex'], location='etherscan log query', ) # Try to see if the event is a duplicate that got returned # in the previous iteration for previous_event in reversed(events): if previous_event['blockNumber'] < block_number: break same_event = (previous_event['logIndex'] == log_index and previous_event['transactionHash'] == event['transactionHash']) if same_event: events.pop() new_events[e_idx][ 'address'] = deserialize_ethereum_address( event['address'], ) new_events[e_idx]['blockNumber'] = block_number new_events[e_idx][ 'timeStamp'] = deserialize_int_from_hex( symbol=event['timeStamp'], location='etherscan log query', ) new_events[e_idx][ 'gasPrice'] = deserialize_int_from_hex( symbol=event['gasPrice'], location='etherscan log query', ) new_events[e_idx][ 'gasUsed'] = deserialize_int_from_hex( symbol=event['gasUsed'], location='etherscan log query', ) new_events[e_idx]['logIndex'] = log_index new_events[e_idx][ 'transactionIndex'] = deserialize_int_from_hex( symbol=event['transactionIndex'], location='etherscan log query', ) except DeserializationError as e: raise RemoteError( 'Couldnt decode an etherscan event due to {str(e)}}', ) from e # etherscan will only return 1000 events in one go. If more than 1000 # are returned such as when no filter args are provided then continue # the query from the last block if len(new_events) == 1000: start_block = new_events[-1]['blockNumber'] else: start_block = end_block + 1 events.extend(new_events) return events
def get_logs( self, contract_address: ChecksumEthAddress, abi: List, event_name: str, argument_filters: Dict[str, Any], from_block: int, to_block: Union[int, str] = 'latest', ) -> List[Dict[str, Any]]: """Queries logs of an ethereum contract May raise: - RemoteError if etherscan is used and there is a problem with reaching it or with the returned result """ event_abi = find_matching_event_abi(abi=abi, event_name=event_name) _, filter_args = construct_event_filter_params( event_abi=event_abi, abi_codec=Web3().codec, contract_address=contract_address, argument_filters=argument_filters, fromBlock=from_block, toBlock=to_block, ) if event_abi['anonymous']: # web3.py does not handle the anonymous events correctly and adds the first topic filter_args['topics'] = filter_args['topics'][1:] events: List[Dict[str, Any]] = [] start_block = from_block if self.connected: until_block = self.web3.eth.blockNumber if to_block == 'latest' else to_block while start_block <= until_block: filter_args['fromBlock'] = start_block end_block = min(start_block + 250000, until_block) filter_args['toBlock'] = end_block log.debug( 'Querying node for contract event', contract_address=contract_address, event_name=event_name, argument_filters=argument_filters, from_block=filter_args['fromBlock'], to_block=filter_args['toBlock'], ) # WTF: for some reason the first time we get in here the loop resets # to the start without querying eth_getLogs and ends up with double logging new_events = self.web3.eth.getLogs(filter_args) start_block = end_block + 1 events.extend(new_events) else: until_block = (self.etherscan.get_latest_block_number() if to_block == 'latest' else to_block) while start_block <= until_block: end_block = min(start_block + 300000, until_block) new_events = self.etherscan.get_logs( contract_address=contract_address, topics=filter_args['topics'], from_block=start_block, to_block=end_block, ) start_block = end_block + 1 events.extend(new_events) return events
def _get_logs( self, web3: Optional[Web3], contract_address: ChecksumEthAddress, abi: List, event_name: str, argument_filters: Dict[str, Any], from_block: int, to_block: Union[int, Literal['latest']] = 'latest', ) -> List[Dict[str, Any]]: """Queries logs of an ethereum contract May raise: - RemoteError if etherscan is used and there is a problem with reaching it or with the returned result """ event_abi = find_matching_event_abi(abi=abi, event_name=event_name) _, filter_args = construct_event_filter_params( event_abi=event_abi, abi_codec=Web3().codec, contract_address=contract_address, argument_filters=argument_filters, fromBlock=from_block, toBlock=to_block, ) if event_abi['anonymous']: # web3.py does not handle the anonymous events correctly and adds the first topic filter_args['topics'] = filter_args['topics'][1:] events: List[Dict[str, Any]] = [] start_block = from_block if web3 is not None: until_block = web3.eth.blockNumber if to_block == 'latest' else to_block while start_block <= until_block: filter_args['fromBlock'] = start_block end_block = min(start_block + 250000, until_block) filter_args['toBlock'] = end_block log.debug( 'Querying node for contract event', contract_address=contract_address, event_name=event_name, argument_filters=argument_filters, from_block=filter_args['fromBlock'], to_block=filter_args['toBlock'], ) # WTF: for some reason the first time we get in here the loop resets # to the start without querying eth_getLogs and ends up with double logging new_events_web3 = cast(List[Dict[str, Any]], web3.eth.getLogs(filter_args)) # Turn all HexBytes into hex strings for e_idx, event in enumerate(new_events_web3): new_events_web3[e_idx]['blockHash'] = event[ 'blockHash'].hex() new_topics = [] for topic in (event['topics']): new_topics.append(topic.hex()) new_events_web3[e_idx]['topics'] = new_topics new_events_web3[e_idx]['transactionHash'] = event[ 'transactionHash'].hex() start_block = end_block + 1 events.extend(new_events_web3) else: # etherscan until_block = (self.etherscan.get_latest_block_number() if to_block == 'latest' else to_block) while start_block <= until_block: end_block = min(start_block + 300000, until_block) new_events = self.etherscan.get_logs( contract_address=contract_address, topics=filter_args['topics'], # type: ignore from_block=start_block, to_block=end_block, ) # Turn all Hex ints to ints for e_idx, event in enumerate(new_events): try: new_events[e_idx]['address'] = to_checksum_address( event['address']) new_events[e_idx][ 'blockNumber'] = deserialize_int_from_hex( symbol=event['blockNumber'], location='etherscan log query', ) new_events[e_idx][ 'timeStamp'] = deserialize_int_from_hex( symbol=event['timeStamp'], location='etherscan log query', ) new_events[e_idx][ 'gasPrice'] = deserialize_int_from_hex( symbol=event['gasPrice'], location='etherscan log query', ) new_events[e_idx][ 'gasUsed'] = deserialize_int_from_hex( symbol=event['gasUsed'], location='etherscan log query', ) new_events[e_idx][ 'logIndex'] = deserialize_int_from_hex( symbol=event['logIndex'], location='etherscan log query', ) new_events[e_idx][ 'transactionIndex'] = deserialize_int_from_hex( symbol=event['transactionIndex'], location='etherscan log query', ) except DeserializationError as e: raise RemoteError( 'Couldnt decode an etherscan event due to {str(e)}}', ) from e start_block = end_block + 1 events.extend(new_events) return events
def _get_event_abi(cls): return find_matching_event_abi(cls.contract_abi, event_name=cls.event_name)
def _find_matching_event_abi(cls, event_name=None, argument_names=None): return find_matching_event_abi(abi=cls.abi, event_name=event_name, argument_names=argument_names)
def _find_matching_event_abi(cls, event_name=None, argument_names=None): return find_matching_event_abi( abi=cls.abi, event_name=event_name, argument_names=argument_names)
def _get_event_abi(cls): return find_matching_event_abi( cls.contract_abi, event_name=cls.event_name)