async def test_bulk(self, *, parity):
        client = JsonRPCClient(parity.dsn()['url'])

        bulk = client.bulk()
        f1 = bulk.eth_blockNumber()
        f2 = bulk.eth_getBalance(FAUCET_ADDRESS)
        f3 = bulk.eth_gasPrice()
        f4 = bulk.eth_getBalance("0x0000000000000000000000000000000000000000")
        f5 = bulk.eth_getBalance(FAUCET_ADDRESS, block=100000000)
        results = await bulk.execute()
        self.assertEqual(f1.result(), results[0])
        self.assertEqual(f2.result(), results[1])
        self.assertEqual(f3.result(), results[2])
        self.assertEqual(f4.result(), results[3])

        try:
            f5.result()
            self.fail("expected exception")
        except JsonRPCError as e:
            self.assertEqual(e.message, "Unknown block number")
        except Exception as e:
            self.fail("unexpected exception: {}".format(e))
        await client.close()
Example #2
0
class HealthMonitor:
    def __init__(self):
        configure_logger(log)

        if 'monitor' in config:
            node_url = config['monitor']['url']
        else:
            log.warning("monitor using config['ethereum'] node")
            node_url = config['ethereum']['url']

        self.eth = JsonRPCClient(node_url, should_retry=True)

    def start(self):
        if not hasattr(self, '_startup_future'):
            self._startup_future = asyncio.get_event_loop().create_future()
            asyncio.get_event_loop().create_task(self._initialise())
            asyncio.get_event_loop().call_later(
                INITIAL_WAIT_CALLBACK_TIME, lambda: asyncio.get_event_loop().
                create_task(self.run_erc20_health_check()))
        return self._startup_future

    @log_unhandled_exceptions(logger=log)
    async def _initialise(self):
        # prepare databases
        self.pool = await prepare_database(handle_migration=False)
        await prepare_redis()

        self._startup_future.set_result(True)

    async def run_erc20_health_check(self):
        try:
            await self._run_erc20_health_check()
        except:
            log.exception("Error running health check")
        asyncio.get_event_loop().call_later(
            ERC20_CHECK_CALLBACK_TIME, lambda: asyncio.get_event_loop().
            create_task(self.run_erc20_health_check()))

    @log_unhandled_exceptions(logger=log)
    async def _run_erc20_health_check(self):

        log.info("running erc20 health check")
        async with self.pool.acquire() as con:
            token_balances = await con.fetch("SELECT * FROM token_balances")

        bad = 0
        requests = []
        last_execute = 0
        bulk = self.eth.bulk()

        for token in token_balances:
            contract_address = token['contract_address']
            data = "0x70a08231000000000000000000000000" + token['eth_address'][
                2:]

            f = bulk.eth_call(to_address=contract_address, data=data)
            requests.append(
                (contract_address, token['eth_address'], f, token['value']))

            if len(requests) >= last_execute + 500:
                await bulk.execute()
                bulk = self.eth.bulk()
                last_execute = len(requests)

        if len(requests) > last_execute:
            await bulk.execute()

        bad_data = {}
        for contract_address, eth_address, f, db_value in requests:
            if not f.done():
                log.warning("future not done when checking erc20 cache")
                continue
            try:
                value = f.result()
            except:
                log.exception("error getting erc20 value {}:{}".format(
                    contract_address, eth_address))
                continue
            if parse_int(value) != parse_int(db_value):
                bad += 1
                bad_data.setdefault(eth_address, set()).add(contract_address)

        if bad > 0:
            log.warning(
                "Found {}/{} bad ERC20 caches over {} addresses".format(
                    bad, len(token_balances), len(bad_data)))

            for eth_address in bad_data:
                erc20_dispatcher.update_token_cache("*", eth_address)
                await asyncio.sleep(15)  # don't overload things
Example #3
0
class BlockMonitor:
    def __init__(self):
        configure_logger(log)

        if 'monitor' in config:
            node_url = config['monitor']['url']
        else:
            log.warning("monitor using config['ethereum'] node")
            node_url = config['ethereum']['url']

        self.eth = JsonRPCClient(node_url,
                                 connect_timeout=5.0,
                                 request_timeout=10.0)
        # filter health processes depend on some of the calls failing on the first time
        # so we have a separate client to handle those
        self.filter_eth = JsonRPCClient(node_url,
                                        force_instance=True,
                                        connect_timeout=10.0,
                                        request_timeout=60.0)

        self._check_schedule = None
        self._poll_schedule = None
        self._sanity_check_schedule = None
        self._block_checking_process = None
        self._filter_poll_process = None
        self._sanity_check_process = None
        self._process_unconfirmed_transactions_process = None

        self._new_pending_transaction_filter_id = None
        self._last_saw_new_block = asyncio.get_event_loop().time()
        self._shutdown = False

        self._lastlog = 0
        self._blocktimes = []

    def start(self):
        if not hasattr(self, '_startup_future'):
            self._startup_future = asyncio.get_event_loop().create_future()
            asyncio.get_event_loop().create_task(self._initialise())
            self._sanity_check_schedule = asyncio.get_event_loop().call_later(
                SANITY_CHECK_CALLBACK_TIME, self.run_sanity_check)
        return self._startup_future

    @log_unhandled_exceptions(logger=log)
    async def _initialise(self):
        # prepare databases
        self.pool = await prepare_database(handle_migration=False)
        await prepare_redis()

        async with self.pool.acquire() as con:
            # check for the last non stale block processed
            row = await con.fetchrow(
                "SELECT blocknumber FROM blocks WHERE stale = FALSE ORDER BY blocknumber DESC LIMIT 1"
            )
            if row is None:
                # fall back on old last_blocknumber
                row = await con.fetchrow(
                    "SELECT blocknumber FROM last_blocknumber")
        if row is None:
            # if there was no previous start, get the current block number
            # and start from there
            last_block_number = await self.eth.eth_blockNumber()
            async with self.pool.acquire() as con:
                await con.execute("INSERT INTO last_blocknumber VALUES ($1)",
                                  last_block_number)
        else:
            last_block_number = row['blocknumber']

        self.last_block_number = last_block_number
        self._shutdown = False

        await self.register_filters()

        self.schedule_filter_poll()

        self._startup_future.set_result(True)

    async def register_filters(self):
        if not self._shutdown:
            await self.register_new_pending_transaction_filter()

    async def register_new_pending_transaction_filter(self):
        backoff = 0
        while not self._shutdown:
            try:
                filter_id = await self.filter_eth.eth_newPendingTransactionFilter(
                )
                log.info(
                    "Listening for new pending transactions with filter id: {}"
                    .format(filter_id))
                self._new_pending_transaction_filter_id = filter_id
                self._last_saw_new_pending_transactions = asyncio.get_event_loop(
                ).time()
                return filter_id
            except:
                log.exception("Error registering for new pending transactions")
                if not self._shutdown:
                    backoff = min(backoff + 1, 10)
                    await asyncio.sleep(backoff)

    def schedule_block_check(self, delay=DEFAULT_BLOCK_CHECK_DELAY):
        if self._shutdown:
            return
        self._check_schedule = asyncio.get_event_loop().call_later(
            delay, self.run_block_check)

    def schedule_filter_poll(self, delay=DEFAULT_POLL_DELAY):
        if self._shutdown:
            return
        self._poll_schedule = asyncio.get_event_loop().call_later(
            delay, self.run_filter_poll)

    def run_filter_poll(self):
        if self._shutdown:
            return
        if self._filter_poll_process is not None and not self._filter_poll_process.done(
        ):
            log.debug("filter polling is already running")
            return
        self._filter_poll_process = asyncio.get_event_loop().create_task(
            self.filter_poll())

    def run_block_check(self):
        if self._shutdown:
            return
        if self._block_checking_process is not None and not self._block_checking_process.done(
        ):
            log.debug("Block check is already running")
            return

        self._block_checking_process = asyncio.get_event_loop().create_task(
            self.block_check())

    def run_process_unconfirmed_transactions(self):
        if self._shutdown:
            return
        if self._process_unconfirmed_transactions_process is not None and not self._process_unconfirmed_transactions_process.done(
        ):
            log.debug("Process unconfirmed transactions is already running")
            return

        self._process_unconfirmed_transactions_process = asyncio.get_event_loop(
        ).create_task(self.process_unconfirmed_transactions())

    @log_unhandled_exceptions(logger=log)
    async def block_check(self):
        while not self._shutdown:
            try:
                block = await self.eth.eth_getBlockByNumber(
                    self.last_block_number + 1)
            except:
                log.exception("Failed eth_getBlockByNumber call")
                break
            if block:
                manager_dispatcher.update_default_gas_price(
                    self.last_block_number + 1)
                self._last_saw_new_block = asyncio.get_event_loop().time()
                processing_start_time = asyncio.get_event_loop().time()
                if self._lastlog + 300 < asyncio.get_event_loop().time():
                    self._lastlog = asyncio.get_event_loop().time()
                    log.info("Processing block {}".format(block['number']))
                    if len(self._blocktimes) > 0:
                        log.info(
                            "Average processing time per last {} blocks: {}".
                            format(
                                len(self._blocktimes),
                                sum(self._blocktimes) / len(self._blocktimes)))

                # check for reorg

                async with self.pool.acquire() as con:
                    last_block = await con.fetchrow(
                        "SELECT * FROM blocks WHERE blocknumber = $1",
                        self.last_block_number)
                # if we don't have the previous block, do a quick sanity check to see if there's any blocks lower
                if last_block is None:
                    async with self.pool.acquire() as con:
                        last_block_number = await con.fetchval(
                            "SELECT blocknumber FROM blocks "
                            "WHERE blocknumber < $1 "
                            "ORDER BY blocknumber DESC LIMIT 1",
                            self.last_block_number)
                    if last_block_number:
                        log.warning(
                            "found gap in blocks @ block number: #{}".format(
                                last_block_number + 1))
                        # roll back to the last block number and sync up
                        self.last_block_number = last_block_number
                        continue
                else:
                    # make sure hash of the last block is the same as the current hash's parent block
                    if last_block['hash'] != block['parentHash']:
                        # we have a reorg!
                        success = await self.handle_reorg()
                        if success:
                            continue
                        # if we didn't find a reorg point, continue on as normal to avoid
                        # preventing the system from operating as a whole

                # check if we're reorging
                async with self.pool.acquire() as con:
                    is_reorg = await con.fetchval(
                        "SELECT 1 FROM blocks WHERE blocknumber = $1",
                        self.last_block_number + 1)

                if block['logsBloom'] != "0x" + ("0" * 512):
                    try:
                        logs_list = await self.eth.eth_getLogs(
                            fromBlock=block['number'], toBlock=block['number'])
                    except:
                        log.exception("failed eth_getLogs call")
                        break
                    logs = {}
                    for _log in logs_list:
                        if _log['transactionHash'] not in logs:
                            logs[_log['transactionHash']] = [_log]
                        else:
                            logs[_log['transactionHash']].append(_log)
                else:
                    logs_list = []
                    logs = {}

                process_tx_tasks = []
                for tx in block['transactions']:
                    # send notifications to sender and reciever
                    if tx['hash'] in logs:
                        tx['logs'] = logs[tx['hash']]
                    process_tx_tasks.append(
                        asyncio.get_event_loop().create_task(
                            self.process_transaction(tx, is_reorg=is_reorg)))
                await asyncio.gather(*process_tx_tasks)

                if logs_list:
                    # send notifications for anyone registered
                    async with self.pool.acquire() as con:
                        for event in logs_list:
                            for topic in event['topics']:
                                filters = await con.fetch(
                                    "SELECT * FROM filter_registrations WHERE contract_address = $1 AND topic_id = $2",
                                    event['address'], topic)
                                for filter in filters:
                                    eth_dispatcher.send_filter_notification(
                                        filter['filter_id'], filter['topic'],
                                        event['data'])

                # update the latest block number, only if it is larger than the
                # current block number.
                block_number = parse_int(block['number'])
                if self.last_block_number < block_number:
                    self.last_block_number = block_number

                async with self.pool.acquire() as con:
                    await con.execute(
                        "UPDATE last_blocknumber SET blocknumber = $1 "
                        "WHERE blocknumber < $1", block_number)
                    await con.execute(
                        "INSERT INTO blocks (blocknumber, timestamp, hash, parent_hash) "
                        "VALUES ($1, $2, $3, $4) "
                        "ON CONFLICT (blocknumber) DO UPDATE "
                        "SET timestamp = EXCLUDED.timestamp, hash = EXCLUDED.hash, "
                        "parent_hash = EXCLUDED.parent_hash, stale = FALSE",
                        block_number,
                        parse_int(block['timestamp']) or int(time.time()),
                        block['hash'], block['parentHash'])

                collectibles_dispatcher.notify_new_block(block_number)
                processing_end_time = asyncio.get_event_loop().time()
                self._blocktimes.append(processing_end_time -
                                        processing_start_time)
                if len(self._blocktimes) > 100:
                    self._blocktimes = self._blocktimes[-100:]

            else:

                break

        self._block_checking_process = None

    @log_unhandled_exceptions(logger=log)
    async def filter_poll(self):

        # check for newly added erc20 tokens
        if not self._shutdown:

            async with self.pool.acquire() as con:
                rows = await con.fetch(
                    "SELECT contract_address FROM tokens WHERE ready = FALSE AND custom = FALSE"
                )
                if len(rows) > 0:
                    total_registrations = await con.fetchval(
                        "SELECT COUNT(*) FROM token_registrations")
                else:
                    total_registrations = 0

            for row in rows:
                log.info("Got new erc20 token: {}. updating {} registrations".
                         format(row['contract_address'], total_registrations))

            if len(rows) > 0:
                limit = 1000
                for offset in range(0, total_registrations, limit):
                    async with self.pool.acquire() as con:
                        registrations = await con.fetch(
                            "SELECT eth_address FROM token_registrations OFFSET $1 LIMIT $2",
                            offset, limit)
                    for row in rows:
                        erc20_dispatcher.update_token_cache(
                            row['contract_address'],
                            *[r['eth_address'] for r in registrations])
                async with self.pool.acquire() as con:
                    await con.executemany(
                        "UPDATE tokens SET ready = true WHERE contract_address = $1",
                        [(r['contract_address'], ) for r in rows])

        if not self._shutdown:

            if self._new_pending_transaction_filter_id is not None:
                # get the list of new pending transactions
                try:
                    new_pending_transactions = await self.filter_eth.eth_getFilterChanges(
                        self._new_pending_transaction_filter_id)
                    # add any to the list of unprocessed transactions
                    for tx_hash in new_pending_transactions:
                        await self.redis.hsetnx(
                            UNCONFIRMED_TRANSACTIONS_REDIS_KEY, tx_hash,
                            int(asyncio.get_event_loop().time()))
                except JSONRPC_ERRORS:
                    log.exception("WARNING: unable to connect to server")
                    new_pending_transactions = None

                if new_pending_transactions is None:
                    await self.register_filters()
                elif len(new_pending_transactions) > 0:
                    self._last_saw_new_pending_transactions = asyncio.get_event_loop(
                    ).time()
                else:
                    # make sure the filter timeout period hasn't passed
                    time_since_last_pending_transaction = int(
                        asyncio.get_event_loop().time() -
                        self._last_saw_new_pending_transactions)
                    if time_since_last_pending_transaction > FILTER_TIMEOUT:
                        log.warning(
                            "Haven't seen any new pending transactions for {} seconds"
                            .format(time_since_last_pending_transaction))
                        await self.register_new_pending_transaction_filter()

                if await self.redis.hlen(UNCONFIRMED_TRANSACTIONS_REDIS_KEY
                                         ) > 0:
                    self.run_process_unconfirmed_transactions()

        if not self._shutdown:

            # no need to run this if the block checking process is still running
            if self._block_checking_process is None or self._block_checking_process.done(
            ):
                try:
                    block_number = await self.filter_eth.eth_blockNumber()
                except JSONRPC_ERRORS:
                    log.exception("Error getting current block number")
                    block_number = 0
                if block_number > self.last_block_number and not self._shutdown:
                    self.schedule_block_check()

        self._filter_poll_process = None

        if not self._shutdown:
            self.schedule_filter_poll(1 if (
                await self.redis.hlen(UNCONFIRMED_TRANSACTIONS_REDIS_KEY) > 0
            ) else DEFAULT_POLL_DELAY)

    @log_unhandled_exceptions(logger=log)
    async def process_unconfirmed_transactions(self):

        if self._shutdown:
            return

        # go through all the unmatched transactions that have no match
        unmatched_transactions = await self.redis.hgetall(
            UNCONFIRMED_TRANSACTIONS_REDIS_KEY, encoding="utf-8")
        for tx_hash, created in unmatched_transactions.items():
            age = asyncio.get_event_loop().time() - int(created)
            try:
                tx = await self.eth.eth_getTransactionByHash(tx_hash)
            except JSONRPC_ERRORS:
                log.exception("Error getting transaction")
                tx = None
            if tx is None:
                # if the tx has existed for 60 seconds and not found, assume it was
                # removed from the network before being accepted into a block
                if age >= 60:
                    await self.redis.hdel(UNCONFIRMED_TRANSACTIONS_REDIS_KEY,
                                          tx_hash)
            else:
                await self.redis.hdel(UNCONFIRMED_TRANSACTIONS_REDIS_KEY,
                                      tx_hash)

                # check if the transaction has already been included in a block
                # and if so, ignore this notification as it will be picked up by
                # the confirmed block check and there's no need to send two
                # notifications about it
                if tx['blockNumber'] is not None:
                    continue

                await self.process_transaction(tx)

            if self._shutdown:
                break

        self._process_unconfirmed_transactions_process = None

    @log_unhandled_exceptions(logger=log)
    async def process_transaction(self, transaction, is_reorg=False):

        to_address = transaction['to']
        # make sure we use a valid encoding of "empty" for contract deployments
        if to_address is None:
            to_address = "0x"
        from_address = transaction['from']

        async with self.pool.acquire() as con:
            # find if we have a record of this tx by checking the from address and nonce
            db_txs = await con.fetch(
                "SELECT * FROM transactions WHERE "
                "from_address = $1 AND nonce = $2", from_address,
                parse_int(transaction['nonce']))
            if len(db_txs) > 1:
                # see if one has the same hash
                db_tx = await con.fetchrow(
                    "SELECT * FROM transactions WHERE "
                    "from_address = $1 AND nonce = $2 AND hash = $3 AND (status != 'error' OR status = 'new')",
                    from_address, parse_int(transaction['nonce']),
                    transaction['hash'])
                if db_tx is None:
                    # find if there are any that aren't marked as error
                    no_error = await con.fetch(
                        "SELECT * FROM transactions WHERE "
                        "from_address = $1 AND nonce = $2 AND hash != $3 AND (status != 'error' OR status = 'new')",
                        from_address, parse_int(transaction['nonce']),
                        transaction['hash'])
                    if len(no_error) == 1:
                        db_tx = no_error[0]
                    elif len(no_error) != 0:
                        log.warning(
                            "Multiple transactions from '{}' exist with nonce '{}' in unknown state"
                        )

            elif len(db_txs) == 1:
                db_tx = db_txs[0]
            else:
                db_tx = None

            # if we have a previous transaction, do some checking to see what's going on
            # see if this is an overwritten transaction
            # if the status of the old tx was previously an error, we don't care about it
            # otherwise, we have to notify the interested parties of the overwrite

            if db_tx and db_tx['hash'] != transaction['hash'] and db_tx[
                    'status'] != 'error':

                if db_tx['v'] is not None:
                    log.warning("found overwritten transaction!")
                    log.warning("tx from: {}".format(from_address))
                    log.warning("nonce: {}".format(
                        parse_int(transaction['nonce'])))
                    log.warning("old tx hash: {}".format(db_tx['hash']))
                    log.warning("new tx hash: {}".format(transaction['hash']))

                manager_dispatcher.update_transaction(db_tx['transaction_id'],
                                                      'error')
                db_tx = None

            # if reorg, and the transaction is confirmed, just update which block it was included in
            if is_reorg and db_tx and db_tx['hash'] == transaction[
                    'hash'] and db_tx['status'] == 'confirmed':
                if transaction['blockNumber'] is None:
                    log.error(
                        "Unexpectedly got unconfirmed transaction again after reorg. hash: {}"
                        .format(db_tx['hash']))
                    # this shouldn't really happen. going to log and abort
                    return db_tx['transaction_id']
                new_blocknumber = parse_int(transaction['blockNumber'])
                if new_blocknumber != db_tx['blocknumber']:
                    async with self.pool.acquire() as con:
                        await con.execute(
                            "UPDATE transactions SET blocknumber = $1 "
                            "WHERE transaction_id = $2", new_blocknumber,
                            db_tx['transaction_id'])
                return db_tx['transaction_id']

            # check for erc20 transfers
            erc20_transfers = []
            if transaction['blockNumber'] is not None and \
               'logs' in transaction and \
               len(transaction['logs']) > 0:

                # find any logs with erc20 token related topics
                for _log in transaction['logs']:
                    if len(_log['topics']) > 0:
                        # Transfer(address,address,uint256)
                        if _log['topics'][0] == TRANSFER_TOPIC:
                            # make sure the log address is for one we're interested in
                            is_known_token = await con.fetchval(
                                "SELECT 1 FROM tokens WHERE contract_address = $1",
                                _log['address'])
                            if not is_known_token:
                                continue
                            if len(_log['topics']) == 3 and len(
                                    _log['data']) == 66:
                                # standard erc20 structure
                                erc20_from_address = decode_single(
                                    ('address', '', []),
                                    data_decoder(_log['topics'][1]))
                                erc20_to_address = decode_single(
                                    ('address', '', []),
                                    data_decoder(_log['topics'][2]))
                                erc20_value = decode_abi(['uint256'],
                                                         data_decoder(
                                                             _log['data']))[0]
                            elif len(_log['topics']) == 1 and len(
                                    _log['data']) == 194:
                                # non-indexed style Transfer events
                                erc20_from_address, erc20_to_address, erc20_value = decode_abi(
                                    ['address', 'address', 'uint256'],
                                    data_decoder(_log['data']))
                            else:
                                log.warning(
                                    'Got invalid erc20 Transfer event in tx: {}'
                                    .format(transaction['hash']))
                                continue
                            erc20_is_interesting = await con.fetchval(
                                "SELECT 1 FROM token_registrations "
                                "WHERE eth_address = $1 OR eth_address = $2",
                                erc20_from_address, erc20_to_address)
                            if erc20_is_interesting:
                                erc20_transfers.append(
                                    (_log['address'],
                                     get_transaction_log_index(_log),
                                     erc20_from_address, erc20_to_address,
                                     hex(erc20_value), 'confirmed'))

                        # special checks for WETH, since it's rarely 'Transfer'ed, but we
                        # still need to update it
                        elif (_log['topics'][0] == DEPOSIT_TOPIC
                              or _log['topics'][0] == WITHDRAWAL_TOPIC
                              ) and _log['address'] == WETH_CONTRACT_ADDRESS:
                            eth_address = decode_single(
                                ('address', '', []),
                                data_decoder(_log['topics'][1]))
                            erc20_is_interesting = await con.fetchval(
                                "SELECT 1 FROM token_registrations "
                                "WHERE eth_address = $1", eth_address)
                            if erc20_is_interesting:
                                erc20_value = decode_abi(['uint256'],
                                                         data_decoder(
                                                             _log['data']))[0]
                                if _log['topics'][0] == DEPOSIT_TOPIC:
                                    erc20_to_address = eth_address
                                    erc20_from_address = "0x0000000000000000000000000000000000000000"
                                else:
                                    erc20_to_address = "0x0000000000000000000000000000000000000000"
                                    erc20_from_address = eth_address
                                erc20_transfers.append(
                                    (WETH_CONTRACT_ADDRESS,
                                     get_transaction_log_index(_log),
                                     erc20_from_address, erc20_to_address,
                                     hex(erc20_value), 'confirmed'))

            elif transaction['blockNumber'] is None and db_tx is None:
                # transaction is pending, attempt to guess if this is a token
                # transaction based off it's input
                if transaction['input']:
                    data = transaction['input']
                    if (data.startswith("0xa9059cbb") and len(data)
                            == 138) or (data.startswith("0x23b872dd")
                                        and len(data) == 202):
                        token_value = hex(int(data[-64:], 16))
                        if data.startswith("0x23b872dd"):
                            erc20_from_address = "0x" + data[34:74]
                            erc20_to_address = "0x" + data[98:138]
                        else:
                            erc20_from_address = from_address
                            erc20_to_address = "0x" + data[34:74]
                        erc20_transfers.append(
                            (to_address, 0, erc20_from_address,
                             erc20_to_address, token_value, 'unconfirmed'))
                    # special WETH handling
                    elif data == '0xd0e30db0' and transaction[
                            'to'] == WETH_CONTRACT_ADDRESS:
                        erc20_transfers.append(
                            (WETH_CONTRACT_ADDRESS, 0,
                             "0x0000000000000000000000000000000000000000",
                             transaction['from'], transaction['value'],
                             'unconfirmed'))
                    elif data.startswith('0x2e1a7d4d') and len(data) == 74:
                        token_value = hex(int(data[-64:], 16))
                        erc20_transfers.append(
                            (WETH_CONTRACT_ADDRESS, 0, transaction['from'],
                             "0x0000000000000000000000000000000000000000",
                             token_value, 'unconfirmed'))

            if db_tx:
                is_interesting = True
            else:
                # find out if there is anyone interested in this transaction
                is_interesting = await con.fetchval(
                    "SELECT 1 FROM notification_registrations "
                    "WHERE eth_address = $1 OR eth_address = $2", to_address,
                    from_address)
            if not is_interesting and len(erc20_transfers) > 0:
                for _, _, erc20_from_address, erc20_to_address, _, _ in erc20_transfers:
                    is_interesting = await con.fetchval(
                        "SELECT 1 FROM notification_registrations "
                        "WHERE eth_address = $1 OR eth_address = $2",
                        erc20_to_address, erc20_from_address)
                    if is_interesting:
                        break
                    is_interesting = await con.fetchval(
                        "SELECT 1 FROM token_registrations "
                        "WHERE eth_address = $1 OR eth_address = $2",
                        erc20_to_address, erc20_from_address)
                    if is_interesting:
                        break

            if not is_interesting:
                return

            if db_tx is None:
                # if so, add it to the database and trigger an update
                # add tx to database
                db_tx = await con.fetchrow(
                    "INSERT INTO transactions "
                    "(hash, from_address, to_address, nonce, "
                    "value, gas, gas_price, "
                    "data) "
                    "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "
                    "RETURNING transaction_id", transaction['hash'],
                    from_address, to_address, parse_int(transaction['nonce']),
                    hex(parse_int(transaction['value'])),
                    hex(parse_int(transaction['gas'])),
                    hex(parse_int(transaction['gasPrice'])),
                    transaction['input'])

            for erc20_contract_address, transaction_log_index, erc20_from_address, erc20_to_address, erc20_value, erc20_status in erc20_transfers:
                is_interesting = await con.fetchval(
                    "SELECT 1 FROM notification_registrations "
                    "WHERE eth_address = $1 OR eth_address = $2",
                    erc20_to_address, erc20_from_address)
                if not is_interesting:
                    is_interesting = await con.fetchrow(
                        "SELECT 1 FROM token_registrations "
                        "WHERE eth_address = $1 OR eth_address = $2",
                        erc20_to_address, erc20_from_address)

                if is_interesting:
                    await con.execute(
                        "INSERT INTO token_transactions "
                        "(transaction_id, transaction_log_index, contract_address, from_address, to_address, value, status) "
                        "VALUES ($1, $2, $3, $4, $5, $6, $7) "
                        "ON CONFLICT (transaction_id, transaction_log_index) DO UPDATE "
                        "SET from_address = EXCLUDED.from_address, to_address = EXCLUDED.to_address, value = EXCLUDED.value",
                        db_tx['transaction_id'], transaction_log_index,
                        erc20_contract_address, erc20_from_address,
                        erc20_to_address, erc20_value, erc20_status)

            manager_dispatcher.update_transaction(
                db_tx['transaction_id'], 'confirmed'
                if transaction['blockNumber'] is not None else 'unconfirmed')
            return db_tx['transaction_id']

    @log_unhandled_exceptions(logger=log)
    async def handle_reorg(self):
        log.info("REORG encounterd at block #{}".format(
            self.last_block_number))
        blocknumber = self.last_block_number
        forked_at_blocknumber = None
        BLOCKS_PER_ITERATION = 10
        while True:
            bulk = self.eth.bulk()
            for i in range(BLOCKS_PER_ITERATION):
                if blocknumber - i >= 0:
                    bulk.eth_getBlockByNumber(blocknumber - i,
                                              with_transactions=True)
            node_results = await bulk.execute()
            async with self.pool.acquire() as con:
                db_results = await con.fetch(
                    "SELECT * FROM blocks WHERE blocknumber <= $1 ORDER BY blocknumber DESC LIMIT $2",
                    blocknumber, BLOCKS_PER_ITERATION)
            while node_results:
                node_block = node_results[0]
                db_block = None
                while db_results:
                    db_block = db_results[0]
                    if parse_int(
                            node_block['number']) != db_block['blocknumber']:
                        log.error(
                            "Got out of order blocks when handling reorg: expected: {}, got: {}"
                            .format(parse_int(node_block['number']),
                                    db_block['blocknumber']))
                        db_results = db_results[1:]
                    else:
                        break
                if db_block is None:
                    # we don't know about any more blocks, so we can just reorg the whole thing!
                    break

                if node_block['hash'] == db_block['hash']:
                    log.info("FORK found at block #{}".format(
                        db_block['blocknumber']))
                    forked_at_blocknumber = db_block['blocknumber']
                    break

                log.info("Mismatched block #{}. old: {}, new: {}".format(
                    db_block['blocknumber'], db_block['hash'],
                    node_block['hash']))

                node_results = node_results[1:]
                db_results = db_results[1:]

            if forked_at_blocknumber is not None:
                break

            blocknumber = blocknumber - BLOCKS_PER_ITERATION
            # if the blocknumber goes too low, abort finding the reorg
            if blocknumber <= 0 or blocknumber < self.last_block_number - 1000:
                log.error("UNABLE TO FIND FORK POINT FOR REORG")
                return False

        if forked_at_blocknumber is None:
            log.error(
                "Error: unexpectedly broke from reorg point finding loop")
            return False

        async with self.pool.acquire() as con:
            # mark blocks as stale
            await con.execute(
                "UPDATE blocks SET stale = TRUE WHERE blocknumber > $1",
                forked_at_blocknumber)
            # revert collectible's last block numbers
            await con.execute(
                "UPDATE collectibles SET last_block = $1 WHERE last_block > $1",
                forked_at_blocknumber - 1)

        self.last_block_number = forked_at_blocknumber
        return True

    def run_sanity_check(self):
        self._sanity_check_process = asyncio.get_event_loop().create_task(
            self.sanity_check())

    @log_unhandled_exceptions(logger=log)
    async def sanity_check(self):
        if self._shutdown:
            return
        # check that filter ids are set to something
        if self._new_pending_transaction_filter_id is None:
            await self.register_new_pending_transaction_filter()
        # check that poll callback is set and not in the past
        if self._poll_schedule is None:
            log.warning("Filter poll schedule is None!")
            self.schedule_filter_poll()
        elif self._filter_poll_process is not None:
            pass
        else:
            if self._poll_schedule._when < self._poll_schedule._loop.time():
                log.warning("Filter poll schedule is in the past!")
                self.schedule_filter_poll()
        # make sure there was a block somewhat recently
        ok = True
        time_since_last_new_block = int(asyncio.get_event_loop().time() -
                                        self._last_saw_new_block)
        if time_since_last_new_block > NEW_BLOCK_TIMEOUT:
            log.warning("Haven't seen any new blocks for {} seconds".format(
                time_since_last_new_block))
            ok = False
        self._sanity_check_schedule = asyncio.get_event_loop().call_later(
            SANITY_CHECK_CALLBACK_TIME, self.run_sanity_check)
        if ok:
            await self.redis.setex("monitor_sanity_check_ok",
                                   SANITY_CHECK_CALLBACK_TIME * 2, "OK")
        self._sanity_check_process = None

    @property
    def redis(self):
        return get_redis_connection()

    async def shutdown(self):

        self._shutdown = True

        try:
            await self.filter_eth.close()
        except:
            pass

        if self._check_schedule:
            self._check_schedule.cancel()
        if self._poll_schedule:
            self._poll_schedule.cancel()
        if self._sanity_check_schedule:
            self._sanity_check_schedule.cancel()

        # let the current iteration of each process finish if running
        if self._block_checking_process:
            await self._block_checking_process
        if self._filter_poll_process:
            await self._filter_poll_process
        if self._sanity_check_process:
            await self._sanity_check_process
        if self._process_unconfirmed_transactions_process:
            await self._process_unconfirmed_transactions_process

        self._startup_future = None