async def update_balances(self): asset_symbols: List[str] = [] asset_update_tasks: List[asyncio.Task] = [] for asset_name, contract in self._erc20_contracts.items(): asset_symbols.append(asset_name) asset_update_tasks.append( self._ev_loop.run_in_executor( wings.get_executor(), contract.functions.balanceOf(self._account_address).call ) ) asset_symbols.append("ETH") asset_update_tasks.append(self._ev_loop.run_in_executor( wings.get_executor(), self._w3.eth.getBalance, self._account_address )) try: asset_raw_balances: List[int] = await asyncio.gather(*asset_update_tasks) for asset_name, raw_balance in zip(asset_symbols, asset_raw_balances): self._raw_account_balances[asset_name] = raw_balance except asyncio.CancelledError: raise except Exception: self.logger().error("Unexpected error fetching account balance updates.", exc_info=True)
async def get_tracking_pairs(self) -> Dict[str, OrderBookTrackerEntry]: # Get the currently active markets sql: SQLConnectionManager = self._sql active_markets: pd.DataFrame = await self.get_active_exchange_markets() # Get the latest order book snapshots from database now: float = time.time() yesterday: float = now - 86400.0 retval: Dict[str, OrderBookTrackerEntry] = {} ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() with sql.engine.connect() as conn: for symbol in active_markets.index: symbol: str = symbol stmt: TextClause = text( self.get_snapshot_message_query(symbol)) try: row = await ev_loop.run_in_executor( wings.get_executor(), lambda: conn.execute(stmt).fetchone()) except DatabaseError: self.logger().warning( "Cannot find last snapshot for %s, skipping.", symbol, exc_info=True) continue if row is None: continue snapshot_msg: OrderBookMessage = self.order_book_class.snapshot_message_from_db( row) snapshot_timestamp: float = row[0] if snapshot_timestamp > yesterday: order_book: OrderBook = self.order_book_class.from_snapshot( snapshot_msg) retval[symbol] = OrderBookTrackerEntry( symbol, snapshot_timestamp, order_book) stmt: TextClause = text( self.get_diff_message_query(symbol)) try: rows = await ev_loop.run_in_executor( wings.get_executor(), lambda: conn.execute( stmt, timestamp=(snapshot_timestamp - 60) * 1e3). fetchall()) for row in rows: diff_msg: OrderBookMessage = self.order_book_class.diff_message_from_db( row) if diff_msg.update_id > order_book.snapshot_uid: order_book.apply_diffs(diff_msg.bids, diff_msg.asks, diff_msg.update_id) except DatabaseError: continue finally: self.logger().debug( "Fetched order book snapshot for %s.", symbol) return retval
async def check_transaction_receipts(self): """ Look for failed transactions, and emit transaction fail event if any are found. """ ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() tasks = [ ev_loop.run_in_executor(wings.get_executor(), self._w3.eth.getTransactionReceipt, tx_hash) for tx_hash in self._pending_tx_dict.keys() ] transaction_receipts: List[AttributeDict] = [ tr for tr in await asyncio.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 = [ ev_loop.run_in_executor(wings.get_executor(), 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 asyncio.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 _get_logs( self, event_filter_params: Dict[str, any], max_tries: Optional[int] = 100) -> List[Dict[str, any]]: ev_loop: asyncio.BaseEventLoop = self._ev_loop count: int = 0 logs = [] while True: try: count += 1 if count > max_tries: self.logger().error( f"Error fetching logs from block with filters: '{event_filter_params}'." ) break logs = await ev_loop.run_in_executor( wings.get_executor(), functools.partial(self._w3.eth.getLogs, event_filter_params)) break except asyncio.CancelledError: raise except Exception: self.logger().debug( f"Block not found with filters: '{event_filter_params}'. Retrying..." ) await asyncio.sleep(0.5) return logs
async def get_timestamp_for_block(self, block_hash: HexBytes, max_tries: Optional[int] = 10) -> int: counter = 0 block: AttributeDict = None if block_hash in self._blocks_window: block = self._blocks_window[block_hash] return block.timestamp else: while block is None: try: if counter == max_tries: raise ValueError( f"Block hash {block_hash.hex()} does not exist.") counter += 1 async with timeout(10.0): ev_loop: asyncio.BaseEventLoop = self._ev_loop block = await ev_loop.run_in_executor( wings.get_executor(), functools.partial(self._w3.eth.getBlock, block_hash, full_transactions=False)) except TimeoutError: self.logger().error( "Timed out fetching new block - '{block_hash}'.", exc_info=True) finally: asyncio.sleep(1.0) return block.timestamp
async def get_block_reorganization( self, incoming_block: AttributeDict) -> List[AttributeDict]: ev_loop: asyncio.BaseEventLoop = self._ev_loop block_reorganization: List[AttributeDict] = [] expected_parent_hash: HexBytes = incoming_block.parentHash while expected_parent_hash not in self._blocks_window and len( block_reorganization) < len(self._blocks_window): replacement_block = None while replacement_block is None: replacement_block = await ev_loop.run_in_executor( wings.get_executor(), functools.partial(self._w3.eth.getBlock, expected_parent_hash, full_transactions=True)) if replacement_block is None: asyncio.sleep(1.0) replacement_block_number: int = replacement_block.number replacement_block_hash: HexBytes = replacement_block.hash replacement_block_parent_hash: HexBytes = replacement_block.parentHash self._block_number_to_hash_map[ replacement_block_number] = replacement_block_hash self._blocks_window[replacement_block_hash] = replacement_block block_reorganization.append(replacement_block) expected_parent_hash = replacement_block_parent_hash block_reorganization.reverse() return block_reorganization
async def call_async(self, func: Callable, *args, timeout_seconds: float = 5.0) -> any: coro: Coroutine = self._ev_loop.run_in_executor( wings.get_executor(), func, *args) return await self.schedule_async_call(coro, timeout_seconds)
async def fetch_new_blocks_loop(self): ev_loop: asyncio.BaseEventLoop = self._ev_loop while True: try: async with timeout(30.0): incoming_block: AttributeDict = await ev_loop.run_in_executor( wings.get_executor(), functools.partial(self._w3.eth.getBlock, self._block_number_to_fetch, full_transactions=True)) if incoming_block is not None: current_block_hash: HexBytes = self._block_number_to_hash_map.get( self._current_block_number, None) incoming_block_hash: HexBytes = incoming_block.hash incoming_block_parent_hash: HexBytes = incoming_block.parentHash new_blocks: List[AttributeDict] = [] if current_block_hash is not None and current_block_hash != incoming_block_parent_hash: block_reorganization: List[ AttributeDict] = await self.get_block_reorganization( incoming_block) new_blocks += block_reorganization self._block_number_to_hash_map[ self._block_number_to_fetch] = incoming_block_hash self._blocks_window[ incoming_block_hash] = incoming_block new_blocks.append(incoming_block) self._current_block_number = self._block_number_to_fetch self._block_number_to_fetch += 1 self.trigger_event(NewBlocksWatcherEvent.NewBlocks, new_blocks) while len( self._blocks_window) > self._block_window_size: block_hash = self._block_number_to_hash_map.popitem( last=False)[1] del self._blocks_window[block_hash] except asyncio.CancelledError: raise except asyncio.TimeoutError: self.logger().error("Timed out fetching new block from node.", exc_info=True) except Exception: self.logger().error("Error fetching new block from node.", exc_info=True) now: float = time.time() next_second: float = (now // 1) + 1 await asyncio.sleep(next_second - now)
async def outgoing_eth_transactions_loop(self): ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() while True: signed_transaction: AttributeDict = await self._outgoing_transactions_queue.get() tx_hash: str = signed_transaction.hash.hex() try: await ev_loop.run_in_executor(wings.get_executor(), self._w3.eth.sendRawTransaction, signed_transaction.rawTransaction) except asyncio.CancelledError: self.logger().error('Cancelled Error', exc_info=True) raise except Exception: self.logger().error(f"Error sending transaction {tx_hash}.", exc_info=True) self.trigger_event(WalletEvent.TransactionFailure, tx_hash) self._local_nonce -= 1
async def check_incoming_eth(self, new_blocks: List[AttributeDict]): watch_addresses: Set[str] = self._watch_addresses filtered_blocks: List[AttributeDict] = [ block for block in new_blocks if block is not None ] block_to_timestamp: Dict[str, float] = dict( (block.hash, float(block.timestamp)) for block in filtered_blocks) transactions: List[AttributeDict] = list( cytoolz.concat(b.transactions for b in filtered_blocks)) incoming_eth_transactions: List[AttributeDict] = [ t for t in transactions if ((t.get("to") in watch_addresses) and (t.get("value", 0) > 0)) ] get_receipt_tasks: List[asyncio.Task] = [ self._ev_loop.run_in_executor(wings.get_executor(), self._w3.eth.getTransactionReceipt, t.hash) for t in incoming_eth_transactions ] transaction_receipts: List[AttributeDict] = await asyncio.gather( *get_receipt_tasks) for incoming_transaction, receipt in zip(incoming_eth_transactions, transaction_receipts): # Filter out failed transactions. if receipt.status != 1: continue # Emit event. raw_eth_value: int = incoming_transaction.get("value") eth_value: float = raw_eth_value * 1e-18 from_address: str = incoming_transaction.get("from") to_address: str = incoming_transaction.get("to") timestamp: float = block_to_timestamp[incoming_transaction.get( "blockHash")] self.trigger_event( IncomingEthWatcherEvent.ReceivedEther, WalletReceivedAssetEvent(timestamp, incoming_transaction.hash.hex(), from_address, to_address, "ETH", eth_value, raw_eth_value))
async def _get_contract_info(self): if self._name is not None and self._symbol is not None and self._decimals is not None: return ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() tasks: List[asyncio.Task] = [ ev_loop.run_in_executor(wings.get_executor(), func, *args) for func, args in [(self.get_name_from_contract, [self._contract]), (self.get_symbol_from_contract, [self._contract]), (self._contract.functions.decimals().call, [])] ] try: name, symbol, decimals = await asyncio.gather(*tasks) self._name = name self._symbol = symbol self._decimals = decimals except asyncio.CancelledError: raise except Exception: self.logger().error( f"Could not fetch token info for {self._contract.address}.", exc_info=True)