Beispiel #1
0
    async def _list_payment_updates(self, address, start_time, end_time=None):

        if end_time is None:
            end_time = datetime.utcnow()
        elif not isinstance(end_time, datetime):
            end_time = datetime.utcfromtimestamp(end_time)
        if not isinstance(start_time, datetime):
            start_time = datetime.utcfromtimestamp(start_time)

        async with self.db:
            txs = await self.db.fetch(
                "SELECT * FROM transactions WHERE "
                "(from_address = $1 OR to_address = $1) AND "
                "updated > $2 AND updated < $3"
                "ORDER BY transaction_id ASC", address, start_time, end_time)
        payments = []
        for tx in txs:
            status = tx['status']
            if status is None or status == 'queued':
                status = 'unconfirmed'
            value = parse_int(tx['value'])
            if value is None:
                value = 0
            else:
                value = hex(value)
            # if the tx was created before the start time, send the unconfirmed
            # message as well.
            if status == 'confirmed' and tx['created'] > start_time:
                payments.append(
                    SofaPayment(status='unconfirmed',
                                txHash=tx['hash'],
                                value=value,
                                fromAddress=tx['from_address'],
                                toAddress=tx['to_address'],
                                networkId=self.application.config['ethereum']
                                ['network_id']).render())
            payments.append(
                SofaPayment(status=status,
                            txHash=tx['hash'],
                            value=value,
                            fromAddress=tx['from_address'],
                            toAddress=tx['to_address'],
                            networkId=self.application.config['ethereum']
                            ['network_id']).render())

        return payments
Beispiel #2
0
    async def get(self, tx_hash):

        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Access-Control-Allow-Headers", "x-requested-with")
        self.set_header('Access-Control-Allow-Methods', 'GET')

        format = self.get_query_argument('format', 'rpc').lower()

        try:
            tx = await ToshiEthJsonRPC(None, self.application,
                                       self.request).get_transaction(tx_hash)
        except JsonRPCError as e:
            raise JSONHTTPError(400, body={'errors': [e.data]})

        if tx is None and format != 'sofa':
            raise JSONHTTPError(
                404,
                body={'error': [{
                    'id': 'not_found',
                    'message': 'Not Found'
                }]})

        if format == 'sofa':

            async with self.db:
                row = await self.db.fetchrow(
                    "SELECT * FROM transactions where hash = $1 ORDER BY transaction_id DESC",
                    tx_hash)
            if row is None:
                raise JSONHTTPError(404,
                                    body={
                                        'error': [{
                                            'id': 'not_found',
                                            'message': 'Not Found'
                                        }]
                                    })
            if tx is None:
                tx = transaction_to_json(
                    database_transaction_to_rlp_transaction(row))
            if row['status'] == 'error':
                tx['error'] = True
            payment = SofaPayment.from_transaction(
                tx,
                networkId=self.application.config['ethereum']['network_id'])
            message = payment.render()
            self.set_header('Content-Type', 'text/plain')
            self.write(message.encode('utf-8'))

        else:

            self.write(tx)
    async def update_transaction(self,
                                 transaction_id,
                                 status,
                                 retry_start_time=0):

        async with self.db:
            tx = await self.db.fetchrow(
                "SELECT * FROM transactions WHERE transaction_id = $1",
                transaction_id)
            if tx is None or tx['status'] == status:
                return

            token_txs = await self.db.fetch(
                "SELECT tok.symbol, tok.name, tok.decimals, tx.contract_address, tx.value, tx.from_address, tx.to_address, tx.transaction_log_index, tx.status "
                "FROM token_transactions tx "
                "JOIN tokens tok "
                "ON tok.contract_address = tx.contract_address "
                "WHERE tx.transaction_id = $1", transaction_id)

            # check if we're trying to update the state of a tx that is already confirmed, we have an issue
            if tx['status'] == 'confirmed':
                log.warning(
                    "Trying to update status of tx {} to {}, but tx is already confirmed"
                    .format(tx['hash'], status))
                return

            # only log if the transaction is internal
            if tx['v'] is not None:
                log.info(
                    "Updating status of tx {} to {} (previously: {})".format(
                        tx['hash'], status, tx['status']))

        if status == 'confirmed':
            try:
                bulk = self.eth.bulk()
                transaction = bulk.eth_getTransactionByHash(tx['hash'])
                tx_receipt = bulk.eth_getTransactionReceipt(tx['hash'])
                await bulk.execute()
                transaction = transaction.result()
                tx_receipt = tx_receipt.result()
            except:
                log.exception("Error getting transaction: {}".format(
                    tx['hash']))
                transaction = None
                tx_receipt = None
            if transaction and 'blockNumber' in transaction and transaction[
                    'blockNumber'] is not None:
                if retry_start_time > 0:
                    log.info(
                        "successfully confirmed tx {} after {} seconds".format(
                            tx['hash'], round(time.time() - retry_start_time,
                                              2)))

                token_tx_updates = []
                updated_token_txs = []
                for token_tx in token_txs:
                    from_address = token_tx['from_address']
                    to_address = token_tx['to_address']
                    # check transaction receipt to make sure the transfer was successful
                    has_transfer_event = False
                    token_tx_status = 'confirmed'
                    if tx_receipt[
                            'logs'] is not None:  # should always be [], but checking just incase
                        for _log in tx_receipt['logs']:
                            if len(_log['topics']) > 0 and _log['topics'][
                                    0] == TRANSFER_TOPIC:
                                if len(_log['topics']) == 3 and len(_log['data']) == 66 and \
                                   decode_single_address(_log['topics'][1]) == from_address and \
                                   decode_single_address(_log['topics'][2]) == to_address:
                                    has_transfer_event = True
                                    break
                                elif len(_log['topics']) == 1 and len(
                                        _log['data']) == 194:
                                    erc20_from_address, erc20_to_address, erc20_value = decode_abi(
                                        ['address', 'address', 'uint256'],
                                        data_decoder(_log['data']))
                                    if erc20_from_address == from_address and \
                                       erc20_to_address == to_address:
                                        has_transfer_event = True
                                        break
                            elif _log['address'] == WETH_CONTRACT_ADDRESS:
                                if _log['topics'][
                                        0] == DEPOSIT_TOPIC and decode_single_address(
                                            _log['topics'][1]) == to_address:
                                    has_transfer_event = True
                                    break
                                elif _log['topics'][
                                        0] == WITHDRAWAL_TOPIC and decode_single_address(
                                            _log['topics'][1]) == from_address:
                                    has_transfer_event = True
                                    break
                        if not has_transfer_event:
                            # there was no Transfer event matching this transaction, this means something went wrong
                            token_tx_status = 'error'
                        else:
                            erc20_dispatcher.update_token_cache(
                                token_tx['contract_address'],
                                from_address,
                                to_address,
                                blocknumber=parse_int(
                                    transaction['blockNumber']))
                    else:
                        log.error(
                            "Unexpectedly got null for tx receipt logs for tx: {}"
                            .format(tx['hash']))
                        token_tx_status = 'error'
                    token_tx_updates.append(
                        (token_tx_status, tx['transaction_id'],
                         token_tx['transaction_log_index']))
                    token_tx = dict(token_tx)
                    token_tx['status'] = token_tx_status
                    updated_token_txs.append(token_tx)

                token_txs = updated_token_txs
                blocknumber = parse_int(transaction['blockNumber'])
                async with self.db:
                    await self.db.execute(
                        "UPDATE transactions SET status = $1, blocknumber = $2, updated = (now() AT TIME ZONE 'utc') "
                        "WHERE transaction_id = $3", status, blocknumber,
                        transaction_id)
                    if token_tx_updates:
                        await self.db.executemany(
                            "UPDATE token_transactions SET status = $1 "
                            "WHERE transaction_id = $2 AND transaction_log_index = $3",
                            token_tx_updates)
                    await self.db.commit()
            else:
                # this is probably because the node hasn't caught up with the latest block yet, retry in a "bit" (but only retry up to 60 seconds)
                if retry_start_time > 0 and time.time(
                ) - retry_start_time >= 60:
                    if transaction is None:
                        log.error(
                            "requested transaction {}'s status to be set to confirmed, but cannot find the transaction"
                            .format(tx['hash']))
                    else:
                        log.error(
                            "requested transaction {}'s status to be set to confirmed, but transaction is not confirmed on the node"
                            .format(tx['hash']))
                    return
                await asyncio.sleep(random.random())
                manager_dispatcher.update_transaction(
                    transaction_id,
                    status,
                    retry_start_time=retry_start_time or time.time())
                return
        else:
            async with self.db:
                await self.db.execute(
                    "UPDATE transactions SET status = $1, updated = (now() AT TIME ZONE 'utc') WHERE transaction_id = $2",
                    status, transaction_id)
                await self.db.commit()

        # render notification

        # don't send "queued"
        if status == 'queued':
            status = 'unconfirmed'
        elif status == 'unconfirmed' and tx['status'] == 'queued':
            # there's already been a tx for this so no need to send another
            return

        messages = []

        # check if this is an erc20 transaction, if so use those values
        if token_txs:
            for token_tx in token_txs:
                token_tx_status = token_tx['status']
                from_address = token_tx['from_address']
                to_address = token_tx['to_address']

                # TokenPayment PNs are not shown at the moment, so i'm removing
                # this for the time being until they're required

                # if token_tx_status == 'confirmed':
                #     data = {
                #         "txHash": tx['hash'],
                #         "fromAddress": from_address,
                #         "toAddress": to_address,
                #         "status": token_tx_status,
                #         "value": token_tx['value'],
                #         "contractAddress": token_tx['contract_address']
                #     }
                #     messages.append((from_address, to_address, token_tx_status, "SOFA::TokenPayment: " + json_encode(data)))

                # if a WETH deposit or withdrawal, we need to let the client know to
                # update their ETHER balance using a normal SOFA:Payment
                if token_tx['contract_address'] == WETH_CONTRACT_ADDRESS and (
                        from_address
                        == "0x0000000000000000000000000000000000000000"
                        or to_address
                        == "0x0000000000000000000000000000000000000000"):
                    payment = SofaPayment(
                        value=parse_int(token_tx['value']),
                        txHash=tx['hash'],
                        status=status,
                        fromAddress=from_address,
                        toAddress=to_address,
                        networkId=config['ethereum']['network_id'])
                    messages.append(
                        (from_address, to_address, status, payment.render()))

        else:
            from_address = tx['from_address']
            to_address = tx['to_address']
            payment = SofaPayment(value=parse_int(tx['value']),
                                  txHash=tx['hash'],
                                  status=status,
                                  fromAddress=from_address,
                                  toAddress=to_address,
                                  networkId=config['ethereum']['network_id'])
            messages.append(
                (from_address, to_address, status, payment.render()))

        # figure out what addresses need pns
        # from address always needs a pn
        for from_address, to_address, status, message in messages:
            manager_dispatcher.send_notification(from_address, message)

            # no need to check to_address for contract deployments
            if to_address == "0x":
                # TODO: update any notification registrations to be marked as a contract
                return

            # check if this is a brand new tx with no status
            if tx['status'] == 'new':
                # if an error has happened before any PNs have been sent
                # we only need to send the error to the sender, thus we
                # only add 'to' if the new status is not an error
                if status != 'error':
                    manager_dispatcher.send_notification(to_address, message)
            else:
                manager_dispatcher.send_notification(to_address, message)

            # trigger a processing of the to_address's queue incase it has
            # things waiting on this transaction
            manager_dispatcher.process_transaction_queue(to_address)
    async def update_transaction(self, transaction_id, status):

        async with self.db:
            tx = await self.db.fetchrow(
                "SELECT * FROM transactions WHERE transaction_id = $1",
                transaction_id)
            if tx is None or tx['status'] == status:
                return

            token_txs = await self.db.fetch(
                "SELECT tok.symbol, tok.name, tok.decimals, tx.contract_address, tx.value, tx.from_address, tx.to_address, tx.transaction_log_index, tx.status "
                "FROM token_transactions tx "
                "JOIN tokens tok "
                "ON tok.contract_address = tx.contract_address "
                "WHERE tx.transaction_id = $1", transaction_id)

            # check if we're trying to update the state of a tx that is already confirmed, we have an issue
            if tx['status'] == 'confirmed':
                log.warning(
                    "Trying to update status of tx {} to {}, but tx is already confirmed"
                    .format(tx['hash'], status))
                return

            # only log if the transaction is internal
            if tx['v'] is not None:
                log.info(
                    "Updating status of tx {} to {} (previously: {})".format(
                        tx['hash'], status, tx['status']))

        if status == 'confirmed':
            transaction = await self.eth.eth_getTransactionByHash(tx['hash'])
            if transaction and 'blockNumber' in transaction:
                blocknumber = parse_int(transaction['blockNumber'])
                async with self.db:
                    await self.db.execute(
                        "UPDATE transactions SET status = $1, blocknumber = $2, updated = (now() AT TIME ZONE 'utc') "
                        "WHERE transaction_id = $3", status, blocknumber,
                        transaction_id)
                    await self.db.commit()
            else:
                log.error(
                    "requested transaction '{}''s status to be set to confirmed, but cannot find the transaction"
                    .format(tx['hash']))
        else:
            async with self.db:
                await self.db.execute(
                    "UPDATE transactions SET status = $1, updated = (now() AT TIME ZONE 'utc') WHERE transaction_id = $2",
                    status, transaction_id)
                await self.db.commit()

        # render notification

        # don't send "queued"
        if status == 'queued':
            status = 'unconfirmed'
        elif status == 'unconfirmed' and tx['status'] == 'queued':
            # there's already been a tx for this so no need to send another
            return

        messages = []

        # check if this is an erc20 transaction, if so use those values
        if token_txs:
            if status == 'confirmed':
                tx_receipt = await self.eth.eth_getTransactionReceipt(
                    tx['hash'])
                if tx_receipt is None:
                    log.error(
                        "Failed to get transaction receipt for confirmed transaction: {}"
                        .format(tx_receipt))
                    # requeue to try again
                    manager_dispatcher.update_transaction(
                        transaction_id, status)
                    return
            for token_tx in token_txs:
                token_tx_status = status
                from_address = token_tx['from_address']
                to_address = token_tx['to_address']
                if status == 'confirmed':
                    # check transaction receipt to make sure the transfer was successful
                    has_transfer_event = False
                    if tx_receipt[
                            'logs'] is not None:  # should always be [], but checking just incase
                        for _log in tx_receipt['logs']:
                            if len(_log['topics']) > 2:
                                if _log['topics'][0] == TRANSFER_TOPIC and \
                                   decode_single_address(_log['topics'][1]) == from_address and \
                                   decode_single_address(_log['topics'][2]) == to_address:
                                    has_transfer_event = True
                                    break
                            elif _log['address'] == WETH_CONTRACT_ADDRESS:
                                if _log['topics'][
                                        0] == DEPOSIT_TOPIC and decode_single_address(
                                            _log['topics'][1]) == to_address:
                                    has_transfer_event = True
                                    break
                                elif _log['topics'][
                                        0] == WITHDRAWAL_TOPIC and decode_single_address(
                                            _log['topics'][1]) == from_address:
                                    has_transfer_event = True
                                    break
                    if not has_transfer_event:
                        # there was no Transfer event matching this transaction
                        token_tx_status = 'error'
                    else:
                        erc20_dispatcher.update_token_cache(
                            token_tx['contract_address'],
                            from_address,
                            to_address,
                            blocknumber=parse_int(transaction['blockNumber']))
                if token_tx_status == 'confirmed':
                    data = {
                        "txHash": tx['hash'],
                        "fromAddress": from_address,
                        "toAddress": to_address,
                        "status": token_tx_status,
                        "value": token_tx['value'],
                        "contractAddress": token_tx['contract_address']
                    }
                    messages.append(
                        (from_address, to_address, token_tx_status,
                         "SOFA::TokenPayment: " + json_encode(data)))
                async with self.db:
                    await self.db.execute(
                        "UPDATE token_transactions SET status = $1 "
                        "WHERE transaction_id = $2 AND transaction_log_index = $3",
                        token_tx_status, tx['transaction_id'],
                        token_tx['transaction_log_index'])
                    await self.db.commit()

                # if a WETH deposit or withdrawal, we need to let the client know to
                # update their ETHER balance using a normal SOFA:Payment
                if token_tx['contract_address'] == WETH_CONTRACT_ADDRESS and (
                        from_address
                        == "0x0000000000000000000000000000000000000000"
                        or to_address
                        == "0x0000000000000000000000000000000000000000"):
                    payment = SofaPayment(
                        value=parse_int(token_tx['value']),
                        txHash=tx['hash'],
                        status=status,
                        fromAddress=from_address,
                        toAddress=to_address,
                        networkId=config['ethereum']['network_id'])
                    messages.append(
                        (from_address, to_address, status, payment.render()))

        else:
            from_address = tx['from_address']
            to_address = tx['to_address']
            payment = SofaPayment(value=parse_int(tx['value']),
                                  txHash=tx['hash'],
                                  status=status,
                                  fromAddress=from_address,
                                  toAddress=to_address,
                                  networkId=config['ethereum']['network_id'])
            messages.append(
                (from_address, to_address, status, payment.render()))

        # figure out what addresses need pns
        # from address always needs a pn
        for from_address, to_address, status, message in messages:
            manager_dispatcher.send_notification(from_address, message)

            # no need to check to_address for contract deployments
            if to_address == "0x":
                # TODO: update any notification registrations to be marked as a contract
                return

            # check if this is a brand new tx with no status
            if tx['status'] is None:
                # if an error has happened before any PNs have been sent
                # we only need to send the error to the sender, thus we
                # only add 'to' if the new status is not an error
                if status != 'error':
                    manager_dispatcher.send_notification(to_address, message)
            else:
                manager_dispatcher.send_notification(to_address, message)

            # trigger a processing of the to_address's queue incase it has
            # things waiting on this transaction
            manager_dispatcher.process_transaction_queue(to_address)
    async def update_transaction(self, transaction_id, status):

        async with self.db:
            tx = await self.db.fetchrow(
                "SELECT * FROM transactions WHERE transaction_id = $1",
                transaction_id)
            if tx is None or tx['status'] == status:
                return

            # check if we're trying to update the state of a tx that is already confirmed, we have an issue
            if tx['status'] == 'confirmed':
                log.warning(
                    "Trying to update status of tx {} to error, but tx is already confirmed"
                    .format(tx['hash']))
                return

            log.info("Updating status of tx {} to {} (previously: {})".format(
                tx['hash'], status, tx['status']))

            if status == 'confirmed':
                transaction = await self.eth.eth_getTransactionByHash(
                    tx['hash'])
                if transaction and 'blockNumber' in transaction:
                    blocknumber = parse_int(transaction['blockNumber'])
                    await self.db.execute(
                        "UPDATE transactions SET status = $1, blocknumber = $2, updated = (now() AT TIME ZONE 'utc') "
                        "WHERE transaction_id = $3", status, blocknumber,
                        transaction_id)
                else:
                    log.error(
                        "requested transaction '{}''s status to be set to confirmed, but cannot find the transaction"
                        .format(tx['hash']))
            else:
                await self.db.execute(
                    "UPDATE transactions SET status = $1, updated = (now() AT TIME ZONE 'utc') WHERE transaction_id = $2",
                    status, transaction_id)
            await self.db.commit()

        # render notification

        # don't send "queued"
        if status == 'queued':
            status = 'unconfirmed'
        elif status == 'unconfirmed' and tx['status'] == 'queued':
            # there's already been a tx for this so no need to send another
            return

        payment = SofaPayment(
            value=parse_int(tx['value']),
            txHash=tx['hash'],
            status=status,
            fromAddress=tx['from_address'],
            toAddress=tx['to_address'],
            networkId=self.application.config['ethereum']['network_id'])
        message = payment.render()

        # figure out what addresses need pns
        # from address always needs a pn
        self.tasks.send_notification(tx['from_address'], message)

        # no need to check to_address for contract deployments
        if tx['to_address'] == "0x":
            # TODO: update any notification registrations to be marked as a contract
            return

        # check if this is a brand new tx with no status
        if tx['status'] is None:
            # if an error has happened before any PNs have been sent
            # we only need to send the error to the sender, thus we
            # only add 'to' if the new status is not an error
            if status != 'error':
                self.tasks.send_notification(tx['to_address'], message)
        else:
            self.tasks.send_notification(tx['to_address'], message)

        # trigger a processing of the to_address's queue incase it has
        # things waiting on this transaction
        self.tasks.process_transaction_queue(tx['to_address'])