class Web3WalletBackend(PubSub): DEFAULT_GAS_PRICE = 1e9 # 1 gwei = 1e9 wei TRANSACTION_RECEIPT_POLLING_TICK = 10.0 _w3wb_logger: Optional[HummingbotLogger] = None @classmethod def logger(cls) -> HummingbotLogger: if cls._w3wb_logger is None: cls._w3wb_logger = logging.getLogger(__name__) return cls._w3wb_logger def __init__(self, private_key: Any, jsonrpc_url: str, erc20_token_addresses: List[str], chain: EthereumChain = EthereumChain.ROPSTEN): super().__init__() # Initialize Web3, accounts and contracts. self._w3: Web3 = Web3(Web3.HTTPProvider(jsonrpc_url)) self._chain: EthereumChain = chain self._account: LocalAccount = Account.privateKeyToAccount(private_key) self._ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() # Initialize ERC20 tokens data structures. self._erc20_token_list: List[ERC20Token] = [ ERC20Token(self._w3, erc20_token_address, self._chain) for erc20_token_address in erc20_token_addresses ] self._erc20_tokens: Dict[str, ERC20Token] = OrderedDict() self._asset_decimals: Dict[str, int] = {"ETH": 18} self._weth_token: Optional[ERC20Token] = None # Initialize the event forwarders. self._received_asset_event_forwarder: EventForwarder = EventForwarder( self._received_asset_event_listener) self._approved_token_event_forwarder: EventForwarder = EventForwarder( self._token_approved_event_listener) self._wrapped_eth_event_forwarder: EventForwarder = EventForwarder( self._eth_wrapped_event_listener) self._unwrapped_eth_event_forwarder: EventForwarder = EventForwarder( self._eth_unwrapped_event_listener) self._zeroex_fill_event_forwarder: EventForwarder = EventForwarder( self._zeroex_fill_event_listener) # Blockchain data self._local_nonce: int = -1 # Watchers self._new_blocks_watcher: Optional[NewBlocksWatcher] = None self._account_balance_watcher: Optional[AccountBalanceWatcher] = None self._erc20_events_watcher: Optional[ERC20EventsWatcher] = None self._incoming_eth_watcher: Optional[IncomingEthWatcher] = None self._weth_watcher: Optional[WethWatcher] = None self._zeroex_fill_watcher: Optional[ZeroExFillWatcher] = None # Tasks and transactions self._check_network_task: Optional[asyncio.Task] = None self._network_status: NetworkStatus = NetworkStatus.STOPPED self._outgoing_transactions_queue: asyncio.Queue = asyncio.Queue() self._outgoing_transactions_task: Optional[asyncio.Task] = None self._check_transaction_receipts_task: Optional[asyncio.Task] = None self._pending_tx_dict: Dict[str, int] = {} self._gas_price: int = self.DEFAULT_GAS_PRICE self._last_timestamp_received_blocks: float = 0.0 self._event_forwarder: EventForwarder = EventForwarder( self._did_receive_new_blocks) @property def address(self) -> str: return self._account.address @property def block_number(self) -> int: return self._new_blocks_watcher.block_number if self._new_blocks_watcher is not None else -1 @property def gas_price(self) -> int: """ Warning: This property causes network access, even though it's synchronous. :return: Gas price in wei """ # TODO: The gas price from Parity is not reliable. Convert to use internal gas price calculator return self._gas_price @property def nonce(self) -> int: """ Warning: This property causes network access, even though it's synchronous. :return: Gas price in wei """ remote_nonce: int = self.get_remote_nonce() retval: int = max(remote_nonce, self._local_nonce) self._local_nonce = retval return retval @property def chain(self) -> EthereumChain: return self._chain @property def erc20_tokens(self) -> Dict[str, ERC20Token]: return self._erc20_tokens.copy() @property def started(self) -> bool: return self._check_network_task is not None @property def network_status(self) -> NetworkStatus: return self._network_status @property def account(self) -> LocalAccount: return self._account @property def zeroex_fill_watcher(self) -> ZeroExFillWatcher: return self._zeroex_fill_watcher def start(self): if self.started: self.stop() self._check_network_task = safe_ensure_future( self._check_network_loop()) self._network_status = NetworkStatus.NOT_CONNECTED def stop(self): if self._check_network_task is not None: self._check_network_task.cancel() self._check_network_task = None safe_ensure_future(self.stop_network()) self._network_status = NetworkStatus.STOPPED async def start_network(self): if self._outgoing_transactions_task is not None: await self.stop_network() async_scheduler: AsyncCallScheduler = AsyncCallScheduler.shared_instance( ) if len(self._erc20_tokens) < len(self._erc20_token_list): # Fetch token data. fetch_symbols_tasks: List[Coroutine] = [ token.get_symbol() for token in self._erc20_token_list ] fetch_decimals_tasks: List[Coroutine] = [ token.get_decimals() for token in self._erc20_token_list ] token_symbols: List[str] = await safe_gather(*fetch_symbols_tasks) token_decimals: List[int] = await safe_gather(*fetch_decimals_tasks ) for token, symbol, decimals in zip(self._erc20_token_list, token_symbols, token_decimals): self._erc20_tokens[symbol] = token self._asset_decimals[symbol] = decimals self._weth_token = self._erc20_tokens.get("WETH") # Fetch blockchain data. self._local_nonce = await async_scheduler.call_async( lambda: self.get_remote_nonce()) # Create event watchers. self._new_blocks_watcher = NewBlocksWatcher(self._w3) self._account_balance_watcher = AccountBalanceWatcher( self._w3, self._new_blocks_watcher, self._account.address, [ erc20_token.address for erc20_token in self._erc20_tokens.values() ], [token.abi for token in self._erc20_tokens.values()]) self._erc20_events_watcher = ERC20EventsWatcher( self._w3, self._new_blocks_watcher, [token.address for token in self._erc20_tokens.values()], [token.abi for token in self._erc20_tokens.values()], [self._account.address]) self._incoming_eth_watcher = IncomingEthWatcher( self._w3, self._new_blocks_watcher, [self._account.address]) if self._weth_token is not None: self._weth_watcher = WethWatcher(self._w3, self._weth_token, self._new_blocks_watcher, [self._account.address]) self._zeroex_fill_watcher = ZeroExFillWatcher( self._w3, self._new_blocks_watcher) # Connect the event forwarders. self._new_blocks_watcher.add_listener(NewBlocksWatcherEvent.NewBlocks, self._event_forwarder) self._erc20_events_watcher.add_listener( ERC20WatcherEvent.ReceivedToken, self._received_asset_event_forwarder) self._erc20_events_watcher.add_listener( ERC20WatcherEvent.ApprovedToken, self._approved_token_event_forwarder) self._incoming_eth_watcher.add_listener( IncomingEthWatcherEvent.ReceivedEther, self._received_asset_event_forwarder) self._zeroex_fill_watcher.add_listener( ZeroExEvent.Fill, self._zeroex_fill_event_forwarder) if self._weth_watcher is not None: self._weth_watcher.add_listener(WalletEvent.WrappedEth, self._wrapped_eth_event_forwarder) self._weth_watcher.add_listener( WalletEvent.UnwrappedEth, self._unwrapped_eth_event_forwarder) # Start the transaction processing tasks. self._outgoing_transactions_task = safe_ensure_future( self.outgoing_eth_transactions_loop()) self._check_transaction_receipts_task = safe_ensure_future( self.check_transaction_receipts_loop()) # Start the event watchers. await self._new_blocks_watcher.start_network() await self._account_balance_watcher.start_network() await self._erc20_events_watcher.start_network() await self._incoming_eth_watcher.start_network() if self._weth_watcher is not None: await self._weth_watcher.start_network() async def stop_network(self): # Disconnect the event forwarders. if self._erc20_events_watcher is not None: self._erc20_events_watcher.remove_listener( ERC20WatcherEvent.ReceivedToken, self._received_asset_event_forwarder) self._erc20_events_watcher.remove_listener( ERC20WatcherEvent.ApprovedToken, self._approved_token_event_forwarder) if self._incoming_eth_watcher is not None: self._incoming_eth_watcher.remove_listener( IncomingEthWatcherEvent.ReceivedEther, self._received_asset_event_forwarder) if self._weth_watcher is not None: self._weth_watcher.remove_listener( WalletEvent.WrappedEth, self._wrapped_eth_event_forwarder) self._weth_watcher.remove_listener( WalletEvent.UnwrappedEth, self._unwrapped_eth_event_forwarder) # Stop the event watchers. if self._new_blocks_watcher is not None: self._new_blocks_watcher.remove_listener( NewBlocksWatcherEvent.NewBlocks, self._event_forwarder) await self._new_blocks_watcher.stop_network() if self._account_balance_watcher is not None: await self._account_balance_watcher.stop_network() if self._erc20_events_watcher is not None: await self._erc20_events_watcher.stop_network() if self._incoming_eth_watcher is not None: await self._incoming_eth_watcher.stop_network() if self._weth_watcher is not None: await self._weth_watcher.stop_network() if self._zeroex_fill_watcher is not None: await self._zeroex_fill_watcher.stop_network() # Stop the transaction processing tasks. if self._outgoing_transactions_task is not None: self._outgoing_transactions_task.cancel() self._outgoing_transactions_task = None if self._check_transaction_receipts_task is not None: self._check_transaction_receipts_task.cancel() self._check_transaction_receipts_task = None async def check_network(self) -> NetworkStatus: # Assume connected if received new blocks in last 2 minutes if time.time() - self._last_timestamp_received_blocks < 60 * 2: return NetworkStatus.CONNECTED try: await self._update_gas_price() except asyncio.CancelledError: raise except Exception: return NetworkStatus.NOT_CONNECTED return NetworkStatus.CONNECTED async def _check_network_loop(self): while True: try: new_status = await asyncio.wait_for(self.check_network(), timeout=10.0) except asyncio.CancelledError: raise except asyncio.TimeoutError: new_status = NetworkStatus.NOT_CONNECTED except Exception: self.logger().network( f"Unexpected error while checking for network status.", exc_info=True, app_warning_msg= f"Unexpected error while checking for network status. " f"Check wallet network connection") new_status = NetworkStatus.NOT_CONNECTED self._network_status = new_status await asyncio.sleep(5.0) async def check_transaction_receipts_loop(self): while True: try: await self.check_transaction_receipts() now: float = time.time() next_tick: float = ( (now // self.TRANSACTION_RECEIPT_POLLING_TICK + 1) * self.TRANSACTION_RECEIPT_POLLING_TICK) await asyncio.sleep(next_tick - now) except asyncio.CancelledError: raise except Exception: self.logger().network( f"Unknown error occurred while checking for transaction receipts.", exc_info=True, app_warning_msg= f"Unknown error occurred while checking for transaction receipts. " f"Check wallet network connection") await asyncio.sleep(5.0) async def check_transaction_receipts(self): """ Look for failed transactions, and emit transaction fail event if any are found. """ async_scheduler: AsyncCallScheduler = AsyncCallScheduler.shared_instance( ) tasks = [ async_scheduler.call_async(self._w3.eth.getTransactionReceipt, tx_hash) for tx_hash in self._pending_tx_dict.keys() ] transaction_receipts: List[AttributeDict] = [ tr for tr in await safe_gather(*tasks) if (tr is not None and tr.get("blockHash") is not None) ] block_hash_set: Set[HexBytes] = set(tr.blockHash for tr in transaction_receipts) fetch_block_tasks = [ async_scheduler.call_async(self._w3.eth.getBlock, block_hash) for block_hash in block_hash_set ] blocks: Dict[HexBytes, AttributeDict] = dict( (block.hash, block) for block in await safe_gather(*fetch_block_tasks) if block is not None) for receipt in transaction_receipts: # Emit gas used event. tx_hash: str = receipt.transactionHash.hex() gas_price_wei: int = self._pending_tx_dict[tx_hash] gas_used: int = receipt.gasUsed gas_eth_amount_raw: int = gas_price_wei * gas_used if receipt.blockHash in blocks: block: AttributeDict = blocks[receipt.blockHash] if receipt.status == 0: self.logger().warning( f"The transaction {tx_hash} has failed.") self.trigger_event(WalletEvent.TransactionFailure, tx_hash) self.trigger_event( WalletEvent.GasUsed, EthereumGasUsedEvent(float(block.timestamp), tx_hash, float(gas_price_wei * 1e-9), gas_price_wei, gas_used, float(gas_eth_amount_raw * 1e-18), gas_eth_amount_raw)) # Stop tracking the transaction. self._stop_tx_tracking(tx_hash) async def outgoing_eth_transactions_loop(self): async_scheduler: AsyncCallScheduler = AsyncCallScheduler.shared_instance( ) while True: signed_transaction: AttributeDict = await self._outgoing_transactions_queue.get( ) tx_hash: str = signed_transaction.hash.hex() try: await async_scheduler.call_async( self._w3.eth.sendRawTransaction, signed_transaction.rawTransaction) except asyncio.CancelledError: raise except Exception: self.logger().network( f"Error sending transaction {tx_hash}.", exc_info=True, app_warning_msg= f"Error sending transaction {tx_hash}. Check wallet network connection" ) self.trigger_event(WalletEvent.TransactionFailure, tx_hash) self._local_nonce -= 1 def _start_tx_tracking(self, tx_hash: str, gas_price: int): self._pending_tx_dict[tx_hash] = gas_price def _stop_tx_tracking(self, tx_hash: str): if tx_hash in self._pending_tx_dict: del self._pending_tx_dict[tx_hash] def schedule_eth_transaction(self, signed_transaction: AttributeDict, gas_price: int): if self._network_status is not NetworkStatus.CONNECTED: raise EnvironmentError( "Cannot send transactions when network status is not connected." ) self._outgoing_transactions_queue.put_nowait(signed_transaction) tx_hash: str = signed_transaction.hash.hex() self._start_tx_tracking(tx_hash, gas_price) self._local_nonce += 1 def get_balance(self, symbol: str) -> Decimal: if self._account_balance_watcher is not None: return self._account_balance_watcher.get_balance(symbol) else: return s_decimal_0 def get_all_balances(self) -> Dict[str, Decimal]: if self._account_balance_watcher is not None: return self._account_balance_watcher.get_all_balances() return {} def get_raw_balance(self, symbol: str) -> int: if self._account_balance_watcher is not None: return self._account_balance_watcher.get_raw_balance(symbol) return 0 def get_raw_balances(self) -> Dict[str, int]: if self._account_balance_watcher is not None: return self._account_balance_watcher.get_raw_balances() return {} def sign_hash(self, text: str = None, hexstr: str = None) -> str: msg_hash: str = defunct_hash_message(hexstr=hexstr, text=text) signature_dict: AttributeDict = self._account.signHash(msg_hash) signature: str = signature_dict["signature"].hex() return signature def execute_transaction(self, contract_function: ContractFunction, **kwargs) -> str: """ This function WILL result in immediate network calls (e.g. to get the gas price, nonce and gas cost), even though it is written in sync manner. :param contract_function: :param kwargs: :return: """ if self._network_status is not NetworkStatus.CONNECTED: raise EnvironmentError( "Cannot send transactions when network status is not connected." ) gas_price: int = self.gas_price transaction_args: Dict[str, Any] = { "from": self.address, "nonce": self.nonce, "chainId": self.chain.value, "gasPrice": gas_price, } transaction_args.update(kwargs) transaction: Dict[str, Any] = contract_function.buildTransaction( transaction_args) if "gas" not in transaction: estimate_gas: int = 1000000 try: estimate_gas = self._w3.eth.estimateGas(transaction) except ValueError: self.logger().error( f"Failed to estimate gas. Using default of 1000000.") transaction["gas"] = estimate_gas signed_transaction: AttributeDict = self._account.signTransaction( transaction) tx_hash: str = signed_transaction.hash.hex() self.schedule_eth_transaction(signed_transaction, gas_price) return tx_hash def send(self, address: str, asset_name: str, amount: Decimal) -> str: """ Warning: This function WILL result in immediate network calls, even though it is written in sync manner. :param address: :param asset_name: :param amount: :return: """ if self._network_status is not NetworkStatus.CONNECTED: raise EnvironmentError( "Cannot send transactions when network status is not connected." ) if asset_name == "ETH": gas_price: int = self.gas_price transaction: Dict[str, Any] = { "to": address, "value": int(amount * 1e18), "gas": 21000, "gasPrice": gas_price, "nonce": self.nonce, "chainId": self.chain.value } signed_transaction: AttributeDict = self._account.signTransaction( transaction) tx_hash: str = signed_transaction.hash.hex() self.schedule_eth_transaction(signed_transaction, gas_price) self.logger().info( f"Sending {amount} ETH from {self.address} to {address}. tx_hash = {tx_hash}." ) return tx_hash else: decimals: int = self._asset_decimals[asset_name] proper_amount: int = int(amount * math.pow(10, decimals)) token_contract: Contract = self.erc20_tokens[asset_name].contract contract_func: ContractFunction = token_contract.functions.transfer( address, proper_amount) tx_hash: str = self.execute_transaction(contract_func) self.logger().info( f"Sending {amount} {asset_name} from {self.address} to {address}. tx_hash = {tx_hash}." ) return tx_hash def approve_token_transfer(self, asset_name: str, spender_address: str, amount: Decimal, **kwargs) -> str: if asset_name not in self.erc20_tokens: raise ValueError( f"{asset_name} is not a known ERC20 token to this wallet.") contract: Contract = self.erc20_tokens[asset_name].contract decimals: int = self._asset_decimals[asset_name] contract_func: ContractFunction = contract.functions.approve( spender_address, int(amount * Decimal(f"1e{decimals}"))) return self.execute_transaction(contract_func, **kwargs) def to_nominal(self, asset_name: str, raw_amount: int) -> Decimal: if asset_name not in self._asset_decimals: raise ValueError(f"Unrecognized asset name '{asset_name}'.") decimals: int = self._asset_decimals[asset_name] return Decimal(raw_amount) * Decimal(f"1e-{decimals}") def to_raw(self, asset_name: str, nominal_amount: Decimal) -> int: if asset_name not in self._asset_decimals: raise ValueError(f"Unrecognized asset name '{asset_name}'.") decimals: int = self._asset_decimals[asset_name] return int(nominal_amount * Decimal(f"1e{decimals}")) @staticmethod def to_raw_static(nominal_amount: Decimal) -> int: return int(nominal_amount * Decimal(f"1e18")) def _received_asset_event_listener( self, received_asset_event: WalletReceivedAssetEvent): self.logger().info( f"Received {received_asset_event.amount_received} {received_asset_event.asset_name} at " f"transaction {received_asset_event.tx_hash}.") self.trigger_event(WalletEvent.ReceivedAsset, received_asset_event) def _token_approved_event_listener( self, token_approved_event: TokenApprovedEvent): self.trigger_event(WalletEvent.TokenApproved, token_approved_event) def _eth_wrapped_event_listener(self, wrapped_eth_event: WalletWrappedEthEvent): self.trigger_event(WalletEvent.WrappedEth, wrapped_eth_event) def _eth_unwrapped_event_listener( self, unwrapped_eth_event: WalletUnwrappedEthEvent): self.trigger_event(WalletEvent.UnwrappedEth, unwrapped_eth_event) def _zeroex_fill_event_listener(self, zeroex_fill_event: ZeroExFillEvent): self.logger().info( f"ZeroEx order {zeroex_fill_event.order_hash} was filled at " f"transaction {zeroex_fill_event.tx_hash}.") self.trigger_event(ZeroExEvent.Fill, zeroex_fill_event) async def check_and_fix_approval_amounts(self, spender: str) -> List[str]: """ Maintain the approve amounts for a token. This function will be used to ensure trade execution using exchange protocols such as 0x, but should be defined in child classes Allowance amounts to manage: 1. Allow external contract to pull tokens from local wallet """ min_approve_amount: int = int(Decimal("1e35")) target_approve_amount: int = int(Decimal("1e36")) async_scheduler: AsyncCallScheduler = AsyncCallScheduler.shared_instance( ) # Get currently approved amounts get_approved_amounts_tasks: List[Coroutine] = [ async_scheduler.call_async( erc20_token.contract.functions.allowance( self.address, spender).call) for erc20_token in self._erc20_token_list ] approved_amounts: List[int] = await safe_gather( *get_approved_amounts_tasks) # Check and fix the approved amounts tx_hashes: List[str] = [] for approved_amount, erc20_token in zip(approved_amounts, self._erc20_token_list): token_name: str = await erc20_token.get_name() token_contract: Contract = erc20_token.contract if approved_amount >= min_approve_amount: self.logger().info( f"Approval already exists for {token_name} from wallet address {self.address}." ) continue self.logger().info( f"Approving spender for drawing {token_name} from wallet address {self.address}." ) tx_hash: str = self.execute_transaction( token_contract.functions.approve(spender, target_approve_amount)) tx_hashes.append(tx_hash) return tx_hashes def wrap_eth(self, amount: Decimal) -> str: if self._weth_token is None: raise EnvironmentError( "No WETH token address was used to initialize this wallet.") contract_func = self._weth_token.contract.functions.deposit() self.logger().info( f"Wrapping {amount} ether from wallet address {self.address}.") return self.execute_transaction(contract_func, value=self.to_raw_static(amount)) def unwrap_eth(self, amount: Decimal) -> str: if self._weth_token is None: raise EnvironmentError( "No WETH token address was used to initialize this wallet.") contract_func = self._weth_token.contract.functions.withdraw( self.to_raw_static(amount)) self.logger().info( f"Unwrapping {amount} ether from wallet address {self.address}.") return self.execute_transaction(contract_func) def _did_receive_new_blocks(self, new_blocks: List[AttributeDict]): self._last_timestamp_received_blocks = time.time() safe_ensure_future(self._update_gas_price()) async def _update_gas_price(self): async_scheduler: AsyncCallScheduler = AsyncCallScheduler.shared_instance( ) new_gas_price: int = await async_scheduler.call_async( getattr, self._w3.eth, "gasPrice") self._gas_price = new_gas_price def get_remote_nonce(self): try: remote_nonce = self._w3.eth.getTransactionCount( self.address, block_identifier="pending") return remote_nonce except BlockNotFound: return None
async def start_network(self): if self._outgoing_transactions_task is not None: await self.stop_network() async_scheduler: AsyncCallScheduler = AsyncCallScheduler.shared_instance( ) if len(self._erc20_tokens) < len(self._erc20_token_list): # Fetch token data. fetch_symbols_tasks: List[Coroutine] = [ token.get_symbol() for token in self._erc20_token_list ] fetch_decimals_tasks: List[Coroutine] = [ token.get_decimals() for token in self._erc20_token_list ] token_symbols: List[str] = await safe_gather(*fetch_symbols_tasks) token_decimals: List[int] = await safe_gather(*fetch_decimals_tasks ) for token, symbol, decimals in zip(self._erc20_token_list, token_symbols, token_decimals): self._erc20_tokens[symbol] = token self._asset_decimals[symbol] = decimals self._weth_token = self._erc20_tokens.get("WETH") # Fetch blockchain data. self._local_nonce = await async_scheduler.call_async( lambda: self.get_remote_nonce()) # Create event watchers. self._new_blocks_watcher = NewBlocksWatcher(self._w3) self._account_balance_watcher = AccountBalanceWatcher( self._w3, self._new_blocks_watcher, self._account.address, [ erc20_token.address for erc20_token in self._erc20_tokens.values() ], [token.abi for token in self._erc20_tokens.values()]) self._erc20_events_watcher = ERC20EventsWatcher( self._w3, self._new_blocks_watcher, [token.address for token in self._erc20_tokens.values()], [token.abi for token in self._erc20_tokens.values()], [self._account.address]) self._incoming_eth_watcher = IncomingEthWatcher( self._w3, self._new_blocks_watcher, [self._account.address]) if self._weth_token is not None: self._weth_watcher = WethWatcher(self._w3, self._weth_token, self._new_blocks_watcher, [self._account.address]) self._zeroex_fill_watcher = ZeroExFillWatcher( self._w3, self._new_blocks_watcher) # Connect the event forwarders. self._new_blocks_watcher.add_listener(NewBlocksWatcherEvent.NewBlocks, self._event_forwarder) self._erc20_events_watcher.add_listener( ERC20WatcherEvent.ReceivedToken, self._received_asset_event_forwarder) self._erc20_events_watcher.add_listener( ERC20WatcherEvent.ApprovedToken, self._approved_token_event_forwarder) self._incoming_eth_watcher.add_listener( IncomingEthWatcherEvent.ReceivedEther, self._received_asset_event_forwarder) self._zeroex_fill_watcher.add_listener( ZeroExEvent.Fill, self._zeroex_fill_event_forwarder) if self._weth_watcher is not None: self._weth_watcher.add_listener(WalletEvent.WrappedEth, self._wrapped_eth_event_forwarder) self._weth_watcher.add_listener( WalletEvent.UnwrappedEth, self._unwrapped_eth_event_forwarder) # Start the transaction processing tasks. self._outgoing_transactions_task = safe_ensure_future( self.outgoing_eth_transactions_loop()) self._check_transaction_receipts_task = safe_ensure_future( self.check_transaction_receipts_loop()) # Start the event watchers. await self._new_blocks_watcher.start_network() await self._account_balance_watcher.start_network() await self._erc20_events_watcher.start_network() await self._incoming_eth_watcher.start_network() if self._weth_watcher is not None: await self._weth_watcher.start_network()