Esempio n. 1
0
 def transaction_already_sent(self, did, did_request, memo):
     rows = Didtx.objects(did=did, status="Processing")
     if rows:
         return rows[0]
     rows = Didtx.objects(did=did, status="Pending")
     if rows:
         row = rows[0]
         row.memo = memo
         row.didRequest = did_request
         row.save()
         return row
     return None
 def transaction_already_sent(self, did, did_request, memo):
     rows = Didtx.objects(did=did)
     if rows:
         for row in rows:
             # Only check transactions that are in Pending state
             if row.status == config.SERVICE_STATUS_PENDING:
                 # Check if header is the same(whether create or update operation)
                 if row.didRequest["header"] == did_request["header"]:
                     # Check if payload is the same(the info to be published)
                     if row.didRequest["payload"] == did_request["payload"]:
                         # Check if memo is the same. If not, just update the row with the new memo passed
                         if row.memo != memo:
                             row.memo = memo
                             row.save()
                     else:
                         # If payload is not the same, update the row with new didRequest
                         row.didRequest = did_request
                         row.save()
                 else:
                     # If header is not the same, update the row with new didRequest
                     row.didRequest = did_request
                     row.save()
                 return row
             # If another transaction for this DID is already Processing, return it because we
             # don't want to create a new request without that first being processed successfully
             elif row.status == config.SERVICE_STATUS_PROCESSING:
                 return row
     return None
 def on_get(self, req, res, did):
     LOG.info(f'Enter /v2/didtx/recent/did/{did}')
     rows = Didtx.objects(did=did.replace("did:elastos:", "").split("#")
                          [0]).order_by('-modified')[:5]
     if rows:
         obj = [each.as_dict() for each in rows]
         self.on_success(res, obj)
     else:
         LOG.info(f"Error /v2/didtx/recent/did/{did}")
         raise NotFoundError()
 def on_get(self, req, res, confirmation_id):
     LOG.info(f'Enter /v2/didtx/confirmation_id/{confirmation_id}')
     try:
         rows = Didtx.objects(id=confirmation_id)
         if rows:
             row = [each.as_dict() for each in rows][0]
             self.on_success(res, row)
         else:
             LOG.info(f"Error /v2/didtx/id/{confirmation_id}")
             raise NotFoundError()
     except Exception as e:
         LOG.info(f"Error /v2/didtx/id/{confirmation_id}: {str(e)}")
         raise NotFoundError()
    def on_post(self, req, res):
        LOG.info(f'Enter /v2/didtx/create')
        data = req.media
        did_request = data["didRequest"]
        memo = data["memo"]

        did_request_payload = did_request["payload"]
        did_request_payload = did_request_payload + "=" * divmod(
            len(did_request_payload), 4)[1]
        did_request_payload = json.loads(
            base64.urlsafe_b64decode(did_request_payload))
        did_request_did = did_request_payload["id"].replace(
            "did:elastos:", "").split("#")[0]

        try:
            caller_did = data["did"].replace("did:elastos:", "").split("#")[0]
            # Verify whether the DID who's making the call, is valid
            did_sidechain_rpc = DidSidechainRpcV2()
            did_resolver_result = did_sidechain_rpc.resolve_did(caller_did)
            if not did_resolver_result:
                err_message = f"Invalid DID: {caller_did}"
                LOG.info(f"Error /v2/didtx/create: {err_message}")
                raise UserNotExistsError(description=err_message)
        except:
            LOG.info(
                f"Info /v2/didtx/create: Defaulting to DID found inside didRequest payload"
            )
            caller_did = did_request_did

        # First verify whether this is a valid payload
        did_publish = Web3DidAdapter()
        tx, err_message = did_publish.create_transaction(
            config.WALLETSV2[0], 1, did_request)
        if err_message:
            err_message = f"Could not generate a valid transaction out of the given didRequest. Error Message: {err_message}"
            LOG.info(f"Error /v2/didtx/create: {err_message}")
            raise InvalidParameterError(description=err_message)

        # Check the number of times this did has used the "did_publish" service
        count = self.retrieve_service_count(caller_did,
                                            config.SERVICE_DIDPUBLISH)
        count_did_request_did = self.retrieve_service_count(
            did_request_did, config.SERVICE_DIDPUBLISH)
        if count_did_request_did > count:
            count = count_did_request_did

        result = {}
        # Check if the row already exists with the same didRequest
        transaction_sent = self.transaction_already_sent(
            caller_did, did_request, memo)
        if transaction_sent:
            result["duplicate"] = True
            result["service_count"] = count
            result["confirmation_id"] = str(transaction_sent.id)
        else:
            result["service_count"] = self.retrieve_service_count(
                caller_did, config.SERVICE_DIDPUBLISH)
            result["duplicate"] = False
            # If less than limit, increment and allow, otherwise, not allowed as max limit is reached
            if count < config.SERVICE_DIDPUBLISH_DAILY_LIMIT:
                row = Didtx(did=caller_did,
                            requestFrom=data["requestFrom"],
                            didRequestDid=did_request_did,
                            didRequest=did_request,
                            memo=memo,
                            version="2",
                            status=config.SERVICE_STATUS_PENDING)
                row.save()
                result["confirmation_id"] = str(row.id)
                self.add_service_count_record(caller_did,
                                              config.SERVICE_DIDPUBLISH)
                self.add_service_count_record(did_request_did,
                                              config.SERVICE_DIDPUBLISH)
            else:
                LOG.info(
                    f"Error /v2/didtx/create: Daily limit reached for this DID"
                )
                raise DailyLimitReachedError()
        self.on_success(res, result)
def cron_send_tx_to_did_sidechain():
    LOG.info('Started cron job: send_tx_to_did_sidechain')
    # Verify the DID sidechain is reachable
    response = did_sidechain_rpc.get_block_count()
    if not (response and response["result"]):
        LOG.info("DID sidechain is currently not reachable...")
        return

    current_height = response["result"] - 1
    # Retrieve the current height from the database
    rows = Didstate.objects()
    if rows:
        row = rows[0]
        # Verify whether a new block has been added since last time
        if current_height > row.currentHeight:
            row.currentHeight = current_height
            row.save()
        else:
            LOG.info(
                "There hasn't been any new block since last cron job was run..."
            )
            return
    else:
        row = Didstate(currentHeight=current_height)
        row.save()

    pending_transactions = []
    current_time = datetime.utcnow().strftime("%a, %b %d, %Y @ %I:%M:%S %p")
    slack_blocks = [{
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": ""
        }
    }, {
        "type": "divider"
    }, {
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": ""
        }
    }, {
        "type": "divider"
    }]
    try:
        # Create raw transactions
        rows_pending = Didtx.objects(status=config.SERVICE_STATUS_PENDING,
                                     version="1")
        for row in rows_pending:
            time_since_created = datetime.utcnow() - row.created
            if (time_since_created.total_seconds() / 60.0) > 60:
                LOG.info(
                    f"The id '{row.id}' with DID '{row.did}' has been in Pending state for the last hour. Changing "
                    f"its state to Cancelled")
                row.status = config.SERVICE_STATUS_CANCELLED
                row.extraInfo = {
                    "reason": "Was in pending state for more than 1 hour"
                }
                row.save()
                continue
            tx = did_publish.create_raw_transaction(row.did, row.didRequest)
            if not tx:
                continue
            tx_decoded = binascii.hexlify(tx).decode(encoding="utf-8")
            pending_transactions.append(tx_decoded)

        if pending_transactions:
            LOG.info("Pending: Found transactions. Sending " +
                     str(len(pending_transactions)) + " transactions to DID "
                     "sidechain now...")
            # Send transaction to DID sidechain
            response = did_sidechain_rpc.send_raw_transaction(
                pending_transactions)
            if not response:
                return
            tx_id = response["result"]
            for row in rows_pending:
                # If for whatever reason, the transactions fail, put them in quarantine and come back to it later
                if tx_id:
                    row.status = config.SERVICE_STATUS_PROCESSING
                    row.blockchainTxId = tx_id
                    LOG.info(
                        "Pending: Successfully sent transaction from wallet: "
                        + did_publish.wallets[did_publish.current_wallet_index]
                        ["address"] + " to the blockchain for id: " +
                        str(row.id) + " DID: " + row.did + " tx_id: " + tx_id)
                else:
                    row.extraInfo = response["error"]
                    row.status = config.SERVICE_STATUS_QUARANTINE
                    LOG.info(
                        "Pending: Error sending transaction from wallet: " +
                        did_publish.wallets[did_publish.current_wallet_index]
                        ["address"] + " for id: " + str(row.id) + " DID:" +
                        row.did + " Error: " + str(row.extraInfo))
                    slack_blocks[0]["text"][
                        "text"] = f"The following transaction was sent to quarantine at {current_time}"
                    slack_blocks[2]["text"]["text"] = f"Wallet used: {did_publish.wallets[did_publish.current_wallet_index]['address']}\n" \
                                                      f"Transaction ID: {str(row.id)}\n"  \
                                                      f"DID: {row.did}\n"  \
                                                      f"Error: {str(row.extraInfo)}"
                    send_slack_notification(slack_blocks)
                row.save()

        # Get info about all the transactions and save them to the database
        rows_processing = Didtx.objects(
            status=config.SERVICE_STATUS_PROCESSING, version="1")
        for row in rows_processing:
            blockchain_tx = did_sidechain_rpc.get_raw_transaction(
                row.blockchainTxId)
            row.blockchainTx = blockchain_tx
            LOG.info("Processing: Blockchain transaction info from wallet: " +
                     did_publish.wallets[
                         did_publish.current_wallet_index]["address"] +
                     " for id: " + str(row.id) + " DID:" + row.did)
            if blockchain_tx["result"]:
                confirmations = blockchain_tx["result"]["confirmations"]
                if confirmations > 2 and row.status != config.SERVICE_STATUS_COMPLETED:
                    row.status = config.SERVICE_STATUS_COMPLETED
                    row.blockchainTx["result"]["confirmations"] = "2+"
            row.save()

        # Try to process quarantined transactions one at a time
        rows_quarantined = Didtx.objects(
            status=config.SERVICE_STATUS_QUARANTINE, version="1")
        binary_split_resend(rows_quarantined)
    except Exception as err:
        message = "Error: " + str(err) + "\n"
        exc_type, exc_obj, exc_tb = sys.exc_info()
        message += "Unexpected error: " + str(exc_type) + "\n"
        message += ' File "' + exc_tb.tb_frame.f_code.co_filename + '", line ' + str(
            exc_tb.tb_lineno) + "\n"
        slack_blocks[0]["text"][
            "text"] = f"Error while sending tx to the blockchain at {current_time}"
        slack_blocks[2]["text"]["text"] = f"Wallet used: {did_publish.wallets[did_publish.current_wallet_index]['address']}\n" \
                                          f"Error: {message}"
        send_slack_notification(slack_blocks)
        LOG.info(f"Error while running cron job: {message}")
    LOG.info('Completed cron job: send_tx_to_did_sidechain')
def cron_send_daily_stats_v2():
    LOG.info('Started cron job: cron_send_daily_stats_v2')
    to_email = config.EMAIL["SENDER"]
    subject = "Assist Backend Daily Stats"
    if config.NETWORK == "testnet":
        subject = "Assist Backend Daily Stats - Testnet"

    current_time = datetime.utcnow().strftime("%a, %b %d, %Y @ %I:%M:%S %p")
    slack_blocks = [{
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": f"These are the daily stats for Assist for {current_time}"
        }
    }, {
        "type": "divider"
    }]

    wallets_stats = "<table><tr><th>Address</th><th>Balance</th><th>Type</th></tr>"
    # Used for testing purposes
    test_address = "0x365b70f14e10b02bef7e463eca6aa3e75ca3cdb1"
    test_balance = "{:.4f}".format(did_sidechain_rpc.get_balance(test_address))
    wallets_stats += f"<tr><td>{test_address}</td><td>{test_balance}</td><td>Testing</td></tr>"
    slack_blocks.append({
        "type": "section",
        "text": {
            "type":
            "mrkdwn",
            "text":
            f"*Wallets and Current Balances*\n {test_address} | {test_balance} | Testing\n"
        }
    })
    for wallet in config.WALLETSV2:
        address = json.loads(wallet)["address"]
        balance = "{:.4f}".format(
            did_sidechain_rpc.get_balance(f"0x{address}"))
        wallets_stats += f"<tr><td>0x{address}</td><td>{balance}</td><td>Production</td></tr>"
        slack_blocks[2]["text"][
            "text"] += f"{address} | {balance} | Production\n"
    wallets_stats += "</table>"

    service_stats = "<table><tr><th>Service</th><th>Users</th><th>Today</th><th>All time</th></tr>"
    slack_blocks.append({
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": f"*Service Stats*\n"
        }
    })
    for service, stats in get_service_count().items():
        service_stats += f"<tr><td>{service}</td><td>{stats['users']}</td><td>{stats['today']}</td><td>{stats['total']}</td></tr>"
        slack_blocks[3]["text"][
            "text"] += f"{service} | {stats['users']} total users | {stats['today']} tx today | {stats['total']} tx total\n"
    service_stats += "</table>"

    didtx_stats = "<table><tr><th>Application</th><th>Today</th><th>All time</th></tr>"
    slack_blocks.append({
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": f"*DID Transactions*\n"
        }
    })
    didtx_by_app = get_didtx_count()
    for app in didtx_by_app["total"].keys():
        didtx_stats += f"<tr><td>{app}</td><td>{didtx_by_app['today'].get(app, 0)}</td><td>{didtx_by_app['total'].get(app, 0)}</td></tr>"
        slack_blocks[4]["text"][
            "text"] += f"{app} | {didtx_by_app['today'].get(app, 0)} tx today | {didtx_by_app['total'].get(app, 0)} tx total\n"
    didtx_stats += "</table>"

    quarantined_transactions = "<table><tr><th>Transaction ID</th><th>DID</th><th>From</th><th>Extra " \
                               "Info</th><th>Created</th></tr>"
    slack_blocks.append({
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": f"*Quarantined Transactions*\n"
        }
    })

    for transaction in Didtx.objects(status=config.SERVICE_STATUS_QUARANTINE,
                                     version='2'):
        id = transaction.id
        did = transaction.did
        request_from = transaction.requestFrom
        created = transaction.created
        extra_info = json.dumps(transaction.extraInfo)
        quarantined_transactions += f"<tr><td>{id}</td><td>{did}</td><td>{request_from}</td><td>{extra_info}</td><td>{created}</td></tr>"
        slack_blocks[5]["text"][
            "text"] += f"{id} | {did} | {request_from} | {extra_info} | {created}\n"
    quarantined_transactions += "</table>"

    slack_blocks.append({"type": "divider"})

    content_html = f"""
        <html>
        <head>
            <style>
            table {{
              font-family: arial, sans-serif;
              border-collapse: collapse;
              width: 100%;
            }}
            
            td, th {{
              border: 1px solid #dddddd;
              text-align: left;
              padding: 8px;
            }}
            
            tr:nth-child(even) {{
              background-color: #dddddd;
            }}
            </style>
        </head>
        <body>
            <h2>These are the daily stats for Assist for {current_time}</h1>
            <h3>Wallets and Current Balances</h3>
            {wallets_stats}
            <h3>Service Stats</h3>
            {service_stats}
            <h3>DID Transactions</h3>
            {didtx_stats}
            <h3>Quarantined Transactions</h3>
            {quarantined_transactions}
        </body>
        </html>
    """
    if config.PRODUCTION:
        send_email(to_email, subject, content_html)
        send_slack_notification(slack_blocks)
    cron_reset_didpublish_daily_limit()
    LOG.info('Completed cron job: cron_send_daily_stats')
def cron_send_tx_to_did_sidechain_v2():
    current_time = datetime.utcnow().strftime("%a, %b %d, %Y @ %I:%M:%S %p")
    LOG.info('Started cron job: cron_send_tx_to_did_sidechain_v2')
    try:
        # Verify the DID sidechain is reachable
        response = did_sidechain_rpc.get_block_count()
        if not response:
            LOG.info("DID sidechain is currently not reachable...")
            return

        rows_pending = Didtx.objects(status=config.SERVICE_STATUS_PENDING,
                                     version='2')
        rows_processing = Didtx.objects(
            status=config.SERVICE_STATUS_PROCESSING, version='2')
        LOG.info(f"rows pending {len(rows_pending)}")

        current_height = response - 1
        # Retrieve the current height from the database
        rows = Didstate.objects()
        if rows:
            row = rows[0]
            # Verify whether a new block has been added since last time
            if current_height > row.currentHeightv2:
                if len(rows_pending) == 0 and len(rows_processing) == 0:
                    LOG.info('Completed cron job: send_tx_to_did_sidechain_v2')
                    return
                row.currentHeightv2 = current_height
                row.save()
            else:
                LOG.info(
                    "There hasn't been any new block since last cron job was run..."
                )
                return
        else:
            row = Didstate(currentHeight=0, currentHeightv2=0)
            row.save()
            return

        slack_blocks = [{
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": ""
            }
        }, {
            "type": "divider"
        }, {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": ""
            }
        }, {
            "type": "divider"
        }]

        # Create raw transactions
        if len(rows_pending) > config.NUM_WALLETSV2:
            rows_pending = rows_pending[:config.NUM_WALLETSV2]
        wallets = config.WALLETSV2

        for index, row in enumerate(list(rows_pending)):
            process_pending_tx(wallets[index], row, slack_blocks, current_time)

        rows_processing = Didtx.objects(
            status=config.SERVICE_STATUS_PROCESSING, version='2')
        LOG.info(f"rows processing {len(rows_processing)}")
        pool = multiprocessing.Pool()
        for row in rows_processing:
            pool.apply_async(process_processing_tx,
                             args=(
                                 row.id,
                                 slack_blocks,
                                 current_time,
                             ))
        pool.close()
        pool.join()
        LOG.info('Completed cron job: send_tx_to_did_sidechain_v2')

    except Exception as err:
        message = "Error: " + str(err) + "\n"
        exc_type, exc_obj, exc_tb = sys.exc_info()
        message += "Unexpected error: " + str(exc_type) + "\n"
        message += ' File "' + exc_tb.tb_frame.f_code.co_filename + '", line ' + str(
            exc_tb.tb_lineno) + "\n"
        LOG.info(f"Error while running cron job: {message}")
        slack_blocks[0]["text"][
            "text"] = f"Error while sending tx to the blockchain at {current_time}"
        slack_blocks[2]["text"]["text"] = f"Error: {message}"
        send_slack_notification(slack_blocks)