예제 #1
0
class NichePaymentHandler(BlockchainMain):

    # Setup the smart contract instance
    smart_contract = None

    nrve_token_symbol = "NRVE"
    niche_payment_address = None
    niche_payment_storage_address = None

    db_config = None
    smtp_config = None

    wallet_needs_recovery = False

    transfers_to_process = []
    transfer_tx_processing = None

    def __init__(self):
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'nrve-niche-config.json'), 'r') as f:
            config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'network-wallets.json'), 'r') as f:
            network_wallets_config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'db-config.json'), 'r') as f:
            self.db_config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'smtp-config.json'), 'r') as f:
            self.smtp_config = json.load(f)

        super().__init__(NetworkType[config['network']],
                         'nrve-niche-payment-handler')

        self.smart_contract = SmartContract(config['smart_contract'])
        self.niche_payment_address = config['niche_payment_address']
        self.niche_payment_storage_address = config[
            'niche_payment_storage_address']

        self.setup_wallet(
            network_wallets_config[config['network']]['wallet_path'])

        # decorate the event handler methods dynamically now that we have loaded the SC
        self.sc_notify = self.smart_contract.on_notify(self.sc_notify)

    def sc_notify(self, event):
        try:
            self.do_sc_notify(event)
        except Exception as e:
            print("Could not process notify event: %s" % e)
            traceback.print_stack()
            traceback.print_exc()
            raise e

    def do_sc_notify(self, event):

        # Make sure that the event payload list has at least one element.
        if not len(event.event_payload):
            self.logger.info(
                "[no event_payload] SmartContract Runtime.Notify event: %s",
                event)
            return

        if event.test_mode:
            self.logger.info(
                "[test_mode] SmartContract Runtime.Notify event: %s", event)
            return

        if not event.execution_success:
            self.logger.info(
                "[execution_success=false] SmartContract Runtime.Notify event: %s",
                event)
            return

        # The event payload list has at least one element. As developer of the smart contract
        # you should know what data-type is in the bytes, and how to decode it. In this example,
        # it's just a string, so we decode it with utf-8:
        event_type = event.event_payload[0].decode("utf-8")

        # only looking for transfer events, so ignore everything else
        if event_type != 'transfer':
            return

        # from, to, amount
        from_address = self.get_address(event.event_payload[1])
        to_address = self.get_address(event.event_payload[2])
        raw_nrve_amount = event.event_payload[3]
        # bl: there can be different data types returned in the amount payload for some reason, so detect which it is (BigInteger/int or bytes)
        if isinstance(raw_nrve_amount, int):
            nrve_amount = raw_nrve_amount
        else:
            nrve_amount = int.from_bytes(raw_nrve_amount, 'little')

        # bl: event.tx_hash is a UInt256, so convert it to a hex string
        tx_hash = event.tx_hash.ToString()

        # if this is an outbound NRVE transfer from our payment wallet, then it's a transfer!
        if from_address == self.niche_payment_address:
            # in order to move on to the next transfer, we just need to clear the tx, assuming it's the right one!
            if self.transfer_tx_processing and tx_hash == self.transfer_tx_processing.ToString(
            ):
                if to_address == self.niche_payment_storage_address:
                    self.logger.info(
                        "- payment storage %s: to %s: %s NRVE (tx: %s)",
                        event_type, to_address, nrve_amount, tx_hash)
                else:
                    self.logger.info("- refund %s: to %s: %s NRVE (tx: %s)",
                                     event_type, to_address, nrve_amount,
                                     tx_hash)
                self.transfer_tx_processing = None
            else:
                log = "%s: to %s: %s NRVE (tx: %s)" % (event_type, to_address,
                                                       nrve_amount, tx_hash)
                self.logger.warn("- unexpected outbound transfer! %s", log)
                self.send_email("Unexpected Outbound Transfer", log)
            return

        # ignore transfers between other accounts. only care about payments to the niche payment address
        if to_address != self.niche_payment_address:
            return

        block_number = event.block_number

        timestamp = self.blockchain.GetHeaderByHeight(block_number).Timestamp

        # Connect to the database
        connection = pymysql.connect(host=self.db_config['host'],
                                     user=self.db_config['user'],
                                     password=self.db_config['password'],
                                     db=self.db_config['db'],
                                     charset='utf8mb4',
                                     cursorclass=pymysql.cursors.DictCursor)

        try:
            with connection.cursor() as cursor:
                log = "- payment %s: from %s: %s NRVE (tx: %s)" % (
                    event_type, from_address, nrve_amount, tx_hash)
                self.logger.info(log)
                sql = ("select oid from `NicheAuctionInvoicePayment`\n"
                       "where fromNeoAddress = %s\n"
                       "and nrveAmount = %s\n"
                       "and paymentStatus = 0\n"
                       "and transactionId is null\n"
                       "for update;")
                args = (from_address, nrve_amount)
                cursor.execute(sql, args)

                if cursor.rowcount == 0:
                    self.logger.error(
                        'Failed identifying payment. Returning to sender: %s',
                        event)
                    self.transfer_payment(from_address, nrve_amount)
                    return
                elif cursor.rowcount > 1:
                    self.logger.error(
                        'FATAL! Identified multiple payments by unique key. Should not be possible! %s',
                        event)
                    self.transfer_payment(from_address, nrve_amount)
                    return

                # when a payment is outstanding, it will be recorded with the expected from address, the proper nrveAmount (in "neurons")
                # and a paymentStatus of 0 which indicates it's pending payment
                sql = ("update `NicheAuctionInvoicePayment`\n"
                       "set transactionId = %s\n"
                       ", transactionDate = from_unixtime(%s)\n"
                       "where fromNeoAddress = %s\n"
                       "and nrveAmount = %s\n"
                       "and paymentStatus = 0\n"
                       "and transactionId is null;")
                args = (tx_hash, timestamp, from_address, nrve_amount)

                # Create a new record
                cursor.execute(sql, args)

                if cursor.rowcount != 1:
                    self.logger.error(
                        'Failed updating payment. Should not be possible since it was already locked for update: %s',
                        event)
                    return

                # send the NRVE to the payment storage address
                self.transfer_payment(self.niche_payment_storage_address,
                                      nrve_amount)

                self.send_email("Successful Niche Payment", log)

            # connection is not autocommit by default. So you must commit to save
            # your changes.
            connection.commit()
        except MySQLError as e:
            error_message = 'ERROR: event %s: {!r}, errno is {}'.format(
                event, e, e.args[0])
            self.logger.error(error_message)
            self.send_email('Niche Payment Error', error_message)
        finally:
            connection.close()

    def send_email(self, subject, body):
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = self.smtp_config['from_address']
        msg['To'] = self.smtp_config['to_address']

        # Send the message via our own SMTP server.
        # bl: production servers user port 587
        s = smtplib.SMTP(self.smtp_config['host'], self.smtp_config['port'])
        if self.smtp_config['use_tls']:
            s.starttls()
        s.send_message(msg)
        s.quit()

    def transfer_payment(self, from_address, nrve_amount):
        self.transfers_to_process.append([from_address, nrve_amount])
        print('transfers_to_process %s', self.transfers_to_process)

    def custom_background_code(self):
        count = 0
        while True:
            sleep(1)

            count += 1
            if (count % 60) == 0:
                self.logger.info("Block %s / %s",
                                 str(Blockchain.Default().Height),
                                 str(Blockchain.Default().HeaderHeight))
                count = 0

            # already have a transfer that we are waiting to process? then just keep waiting until that transaction comes through
            if self.transfer_tx_processing:
                continue

            # no transfers? then keep waiting
            if not self.transfers_to_process:
                continue

            if self.wallet_needs_recovery:
                self.recover_wallet()
                self.wallet_needs_recovery = False
            else:
                self.wallet_sync()

            transfer = self.transfers_to_process[0]
            self.transfers_to_process = self.transfers_to_process[1:]
            if len(transfer) != 2:
                self.logger.error(
                    'ERROR! transfer must have exactly 2 args. skipping! %s',
                    transfer)
                continue

            to_address = transfer[0]
            if to_address == self.niche_payment_storage_address:
                self.logger.debug('processing payment storage: %s', transfer)
            else:
                self.logger.debug('processing refund: %s', transfer)
            token = get_asset_id(self.wallet, self.nrve_token_symbol)
            print('found token %s', token)
            result = do_token_transfer(token, self.wallet,
                                       self.niche_payment_address, to_address,
                                       transfer[1], False)

            if not result:
                # transaction failed? wallet probably out-of-sync (insufficient funds) so reload it
                self.wallet_needs_recovery = True
                # we need to try to process this transfer again, so add it back in to the list
                self.transfers_to_process = [transfer
                                             ] + self.transfers_to_process
            else:
                # transaction successfully relayed? then let's set the tx Hash that we're waiting for
                self.transfer_tx_processing = result.Hash
class BulkProcess(BlockchainMain):
    # from InputParser
    parser = ZeroOrMore(
        Regex(r'\[[^]]*\]') | Regex(r'"[^"]*"') | Regex(r'\'[^\']*\'')
        | Regex(r'[^ ]+'))

    smart_contract_hash = None

    operation = None
    operation_args_array_length = None
    expected_result_count = None
    from_addr = None

    test_only = False

    wallet_needs_recovery = False

    smart_contract = None

    job = None

    jobs = None

    jobs_processed = 0
    tx_processing = None

    def __init__(self):
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'bulk-tx-config.json'), 'r') as f:
            config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', config['job_config_file']), 'r') as f:
            job_config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'network-wallets.json'), 'r') as f:
            network_wallets_config = json.load(f)

        super().__init__(NetworkType[config['network']], 'bulk-process-tx')

        self.test_only = config['test_only']

        self.operation = job_config['operation']
        self.operation_args_array_length = job_config[
            'operation_args_array_length']
        self.expected_result_count = job_config['expected_result_count']
        try:
            self.from_addr = job_config['from_addr']
        except KeyError:
            pass

        self.jobs = job_config['jobs']

        # Setup the smart contract instance
        self.smart_contract_hash = config['smart_contract']
        self.smart_contract = SmartContract(self.smart_contract_hash)

        # decorate the event handler methods dynamically now that we have loaded the SC
        self.sc_notify = self.smart_contract.on_notify(self.sc_notify)
        self.sc_storage = self.smart_contract.on_storage(self.sc_storage)
        self.sc_execution = self.smart_contract.on_execution(self.sc_execution)

        self.setup_wallet(
            network_wallets_config[config['network']]['wallet_path'])

    def pre_start(self):
        # trigger the first job to be processed
        self.process_job()

    def sc_notify(self, event):
        if not event.execution_success:
            return

        prefix = ""
        if event.test_mode:
            prefix = "[test_mode]"
        elif event.tx_hash != self.tx_processing:
            # only emit notify events for the transaction that we are waiting on
            return

        self.logger.info(
            prefix + "[SmartContract.Runtime.Notify] [%s] [tx %s] %s",
            event.contract_hash, event.tx_hash, event.event_payload)

    def sc_storage(self, event):
        prefix = ""
        if event.test_mode:
            prefix = "[test_mode]"
        elif event.tx_hash != self.tx_processing:
            # only emit notify events for the transaction that we are waiting on
            return

        self.logger.info(prefix + "[%s] [%s] [tx %s] %s", event.event_type,
                         event.contract_hash, event.tx_hash,
                         event.event_payload)

    def sc_execution(self, event):
        # only emit execution events for the transaction that we are waiting on
        if event.tx_hash != self.tx_processing:
            return

        if not event.execution_success:
            self.logger.error(
                "[execution_success=false][SmartContract.Runtime.Notify] [%s] [tx %s] %s",
                event.contract_hash, event.tx_hash, event.event_payload)
            return

        prefix = ""
        if event.test_mode:
            prefix = "[test_mode]"

        self.logger.info(
            prefix + "[SmartContract.Execution.Success] [%s] [tx %s] %s",
            event.contract_hash, event.tx_hash, event.event_payload)

        if not event.test_mode:
            self.jobs_processed += 1
            self.process_job()

    def process_job(self):
        jobs_remaining = len(self.jobs)

        self.logger.debug("%s jobs processed. %s jobs remaining.",
                          self.jobs_processed, jobs_remaining)

        self.tx_processing = None

        if jobs_remaining > 0:
            # just pop a job off the array to process next
            self.job = self.jobs[0]
            self.jobs = self.jobs[1:]
        else:
            # change the jobs array to None (from an empty array) to indicate we are done and can shut down
            self.jobs = None

    def custom_background_code(self):
        """ Custom code run in a background thread. Prints the current block height.

        This function is run in a daemonized thread, which means it can be instantly killed at any
        moment, whenever the main thread quits. If you need more safety, don't use a  daemonized
        thread and handle exiting this thread in another way (eg. with signals and events).
        """
        while True:
            sleep(1)

            if not self.job:
                # no more jobs? then shut 'er down!
                if self.jobs is None:
                    self.shutdown()

                # if it's a refund job, then check to see if we have the transaction recorded yet. if not, keep waiting.
                # note that this will give an info log "Could not find transaction for hash b'xxx'" every second until the tx is processed.
                if self.is_refund_job() and self.tx_processing:
                    tx, height = Blockchain.Default().GetTransaction(
                        self.tx_processing)
                    # the tx will have a height once it's completed!
                    if height > -1:
                        # the tx has been processed, so process the next refund!
                        self.jobs_processed += 1
                        self.process_job()
                continue

            if self.wallet_needs_recovery:
                self.recover_wallet()
                self.wallet_needs_recovery = False
            else:
                self.wallet_sync()

            # special handling for sending refunds
            if self.is_refund_job():
                self.process_refund_job()
            else:
                self.process_testinvoke_job()

    def is_refund_job(self):
        return self.operation == 'send'

    def process_refund_job(self):
        if len(self.job) != self.operation_args_array_length:
            self.logger.error(
                'ERROR! must have exactly %d operation args, not %d. skipping! %s',
                self.operation_args_array_length, len(self.job), self.job)
            self.job = None
            self.process_job()
            return

        # bl: tx can fail if there are no connected peers, so wait for one
        self.wait_for_peers()

        self.logger.debug('processing refund: %s', self.job)
        # in case we have to rebuild the wallet and try the job again, pass in a new list to construct_and_send
        # since internally the method actually has a side effect of modifying the array to strip out the from address
        result = construct_and_send(None, self.wallet, list(self.job), False)

        if not result:
            self.wallet_needs_recovery = True
        else:
            self.job = None
            self.tx_processing = result.Hash

    def process_testinvoke_job(self):
        job_args = self.parser.parseString(self.operation + " " +
                                           str(self.job))
        job_args = job_args[0:]

        if len(job_args) != 2:
            self.logger.error(
                'ERROR! must have only 2 args (operation, params). skipping! %s',
                job_args)
            self.job = None
            self.process_job()
            return

        operation_params = parse_param(job_args[1])
        if len(operation_params) != self.operation_args_array_length:
            self.logger.error(
                'ERROR! must have exactly %d operation args, not %d. skipping! %s',
                self.operation_args_array_length, len(operation_params),
                job_args)
            self.job = None
            self.process_job()
            return

        args = [self.smart_contract_hash] + job_args
        self.logger.debug('processing job: %s', args)
        result = self.test_invoke(args, self.expected_result_count,
                                  self.test_only, self.from_addr)

        if not result:
            # transaction failed? wallet probably out-of-sync (insufficient funds) so reload it
            self.wallet_needs_recovery = True
        else:
            # this job has been invoked, so clear it out. on to the next.
            self.job = None
            if self.test_only:
                # when testing but not relaying transactions, we just continue to the next job
                self.jobs_processed += 1
                self.process_job()
            else:
                # transaction successfully relayed? then let's set the tx Hash that we're waiting for
                self.tx_processing = result.Hash
예제 #3
0
class TokenSaleEventHandler(BlockchainMain):

    smart_contract_hash = None

    # Setup the smart contract instance
    smart_contract = None
    old_smart_contract = None

    db_config = None
    smtp_config = None

    ignore_blocks_older_than = None

    disable_auto_whitelist = None

    wallet_needs_recovery = False

    whitelists_to_process = []
    whitelist_tx_processing = None

    def __init__(self, disable_auto_whitelist):
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'neo-nrve-config.json'), 'r') as f:
            config = json.load(f)
        if not disable_auto_whitelist:
            with open(
                    os.path.join(os.path.abspath(os.path.dirname(__file__)),
                                 'config', 'network-wallets.json'), 'r') as f:
                network_wallets_config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'db-config.json'), 'r') as f:
            self.db_config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'smtp-config.json'), 'r') as f:
            self.smtp_config = json.load(f)

        super().__init__(NetworkType[config['network']],
                         'neo-nrve-eventhandler')

        self.smart_contract_hash = config['smart_contract']
        self.smart_contract = SmartContract(self.smart_contract_hash)
        self.old_smart_contract = SmartContract(config['old_smart_contract'])
        self.ignore_blocks_older_than = config['ignore_blocks_older_than']

        # decorate the event handler method dynamically now that we have loaded the SCs
        self.sc_notify = self.old_smart_contract.on_notify(self.sc_notify)
        self.sc_notify = self.smart_contract.on_notify(self.sc_notify)

        if not disable_auto_whitelist:
            self.setup_wallet(
                network_wallets_config[config['network']]['wallet_path'])
        else:
            self.setup_network()

        self.disable_auto_whitelist = disable_auto_whitelist

    def sc_notify(self, event):

        event_payload = event.event_payload

        if not isinstance(
                event_payload, ContractParameter
        ) or event_payload.Type != ContractParameterType.Array:
            self.logger.info(
                "[invalid event_payload] SmartContract Runtime.Notify event: %s",
                event)
            return

        payload = event_payload.Value

        # Make sure that the event payload list has at least one element.
        if not len(payload):
            self.logger.info(
                "[no event_payload] SmartContract Runtime.Notify event: %s",
                event)
            return

        if event.test_mode:
            self.logger.info(
                "[test_mode] SmartContract Runtime.Notify event: %s", event)
            return

        if not event.execution_success:
            self.logger.info(
                "[execution_success=false] SmartContract Runtime.Notify event: %s",
                event)
            return

        # The event payload list has at least one element. As developer of the smart contract
        # you should know what data-type is in the bytes, and how to decode it. In this example,
        # it's just a string, so we decode it with utf-8:
        event_type = payload[0].Value.decode("utf-8")
        block_number = event.block_number

        if self.ignore_blocks_older_than and block_number < self.ignore_blocks_older_than:
            return

        timestamp = self.blockchain.GetHeaderByHeight(block_number).Timestamp

        # bl: event.contract_hash is a UInt160, so convert it to a hex string
        contract_hash = event.contract_hash.ToString()
        # bl: event.tx_hash is a UInt256, so convert it to a hex string
        tx_hash = event.tx_hash.ToString()

        # bl: we only care about refunds for the old smart contract
        if contract_hash == self.old_smart_contract.contract_hash and event_type != 'refund':
            return

        connection = self.get_connection()

        try:
            with connection.cursor() as cursor:
                if event_type == 'kyc_registration' or event_type == 'kyc_deregistration':
                    address = self.get_address(payload[1].Value)
                    self.logger.info("- %s: %s", event_type, address)
                    sql = "update `NarrativeUserNeoAddress` set whitelisted = %s where neoAddress = %s"
                    args = (1 if event_type == 'kyc_registration' else 0,
                            address)
                elif event_type == 'contribution':
                    # from, neo, tokens
                    address = self.get_address(payload[1].Value)
                    # based on the smart contract, we know these should always be whole numbers
                    neo = (int)(payload[2].Value / 100000000)
                    tokens = (int)(payload[3].Value / 100000000)
                    self.logger.info("- %s: %s: %s NEO (%s NRVE) (tx: %s)",
                                     event_type, address, neo, tokens, tx_hash)
                    sql = (
                        "insert into `NarrativeContribution` (transactionId, neo, nrveTokens, transactionDate, neoAddress_oid)\n"
                        "select %s, %s, %s, from_unixtime(%s), na.oid\n"
                        "from NarrativeUserNeoAddress na\n"
                        "where na.neoAddress = %s")
                    args = (tx_hash, neo, tokens, timestamp, address)
                elif event_type == 'refund':
                    # to, amount
                    address = self.get_address(payload[1].Value)
                    # based on the smart contract, the amount should always be a whole number
                    amount = (int)(payload[2].Value / 100000000)
                    log = "%s: %s: %s NEO [%s] (tx: %s)" % (
                        event_type, address, amount, contract_hash, tx_hash)
                    self.logger.info('- ' + log)
                    sql = (
                        "insert into `NarrativeRefund` (transactionId, contractHash, neo, transactionDate, neoAddress)\n"
                        "values (%s, %s, %s, from_unixtime(%s), %s)")
                    args = (tx_hash, contract_hash, amount, timestamp, address)
                    self.send_email("Narrative Refund Required", log)
                elif event_type == 'transfer' or event_type == 'approve':
                    # bl: ignore NEP5 transfers and approvals. don't care about those, and there will be a lot!
                    return
                else:
                    self.logger.warn("Unhandled event: %s", event)
                    return

                # Create a new record
                cursor.execute(sql, args)

                if cursor.rowcount != 1:
                    self.logger.error('ERROR: Failed recording event: %s',
                                      event)

            # connection is not autocommit by default. So you must commit to save
            # your changes.
            connection.commit()
        except MySQLError as e:
            self.logger.error('ERROR: event %s: {!r}, errno is {}'.format(
                event, e, e.args[0]))
        finally:
            connection.close()

        # if this is the whitelist tx we are waiting for, then clear it out so the next can be processed!
        if not self.disable_auto_whitelist and self.whitelist_tx_processing and tx_hash == self.whitelist_tx_processing.ToString(
        ):
            self.whitelist_tx_processing = None

    def get_connection(self):
        # Connect to the database
        return pymysql.connect(host=self.db_config['host'],
                               user=self.db_config['user'],
                               password=self.db_config['password'],
                               db=self.db_config['db'],
                               charset='utf8mb4',
                               cursorclass=pymysql.cursors.DictCursor)

    def send_email(self, subject, body):
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = self.smtp_config['from_address']
        msg['To'] = self.smtp_config['to_address']

        # Send the message via our own SMTP server.
        # bl: production servers user port 587
        s = smtplib.SMTP(self.smtp_config['host'], self.smtp_config['port'])
        if self.smtp_config['use_tls']:
            s.starttls()
        s.send_message(msg)
        s.quit()

    async def custom_background_code(self):
        count = 0
        while True:
            await asyncio.sleep(1)

            count += 1
            if (count % 60) == 0:
                self.logger.info("Block %s / %s",
                                 str(Blockchain.Default().Height),
                                 str(Blockchain.Default().HeaderHeight))
                count = 0

            # when disabling auto-whitelisting, nothing further to do here
            if self.disable_auto_whitelist:
                continue

            # already have a whitelist that we are waiting to process? then just keep waiting until that transaction comes through
            if self.whitelist_tx_processing:
                continue

            # load addresses to whitelist every 15 seconds, but only if the list is empty
            if not self.whitelists_to_process:
                # look for NEO addresses to whitelist every 15 seconds
                if (count % 15) != 0:
                    continue
                self.load_addresses_to_whitelist()

            # no whitelists to process? then keep waiting
            if not self.whitelists_to_process:
                continue

            if self.wallet_needs_recovery:
                await self.recover_wallet()
                self.wallet_needs_recovery = False
            else:
                await self.wallet_sync()

            addresses_to_whitelist = self.whitelists_to_process[0:6]
            self.whitelists_to_process = self.whitelists_to_process[6:]

            self.logger.debug('whitelisting addresses: %s',
                              addresses_to_whitelist)
            result = await self.test_invoke([
                self.smart_contract_hash, 'crowdsale_register',
                str(addresses_to_whitelist)
            ], len(addresses_to_whitelist), False)

            if not result:
                # transaction failed? wallet probably out-of-sync (insufficient funds) so reload it
                self.wallet_needs_recovery = True
                # we need to try to process this refund again, so add it back in to the list
                self.whitelists_to_process = addresses_to_whitelist + self.whitelists_to_process
            else:
                # transaction successfully relayed? then let's set the tx Hash that we're waiting for
                self.whitelist_tx_processing = result.Hash

    def load_addresses_to_whitelist(self):
        connection = self.get_connection()
        try:
            with connection.cursor() as cursor:
                sql = (
                    "select na.neoAddress from `NarrativeUser` u\n"
                    "inner join `NarrativeUserNeoAddress` na on na.oid = u.primaryNeoAddress_oid\n"
                    "where na.whitelisted = 0\n"
                    "and u.hasVerifiedEmailAddress = 1\n"
                    "and u.kycStatus = 3;")
                cursor.execute(sql)

                rows = cursor.fetchall()

                for row in rows:
                    self.whitelists_to_process.append(row['neoAddress'])
        except MySQLError as e:
            self.logger.error(
                'ERROR: selecting whitelist addresses: {!r}, errno is {}'.
                format(e, e.args[0]))
        finally:
            connection.close()
예제 #4
0
class NichePaymentHandler(BlockchainMain):

    # Setup the smart contract instance
    smart_contract = None

    nrve_token_symbol = "NRVE"
    niche_payment_address = None

    db_config = None
    smtp_config = None

    wallet_needs_recovery = False

    transfers_to_process = []
    transfer_tx_processing = None

    def __init__(self):
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'nrve-niche-config.json'), 'r') as f:
            config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'db-config.json'), 'r') as f:
            self.db_config = json.load(f)
        with open(
                os.path.join(os.path.abspath(os.path.dirname(__file__)),
                             'config', 'smtp-config.json'), 'r') as f:
            self.smtp_config = json.load(f)

        super().__init__(NetworkType[config['network']],
                         'nrve-niche-payment-handler')

        self.smart_contract = SmartContract(config['smart_contract'])
        self.niche_payment_address = config['niche_payment_address']

        self.setup_network()

        # decorate the event handler methods dynamically now that we have loaded the SC
        self.sc_notify = self.smart_contract.on_notify(self.sc_notify)

    def sc_notify(self, event):
        try:
            self.do_sc_notify(event)
        except Exception as e:
            print("Could not process notify event: %s" % e)
            traceback.print_stack()
            traceback.print_exc()
            raise e

    def do_sc_notify(self, event):

        event_payload = event.event_payload

        if not isinstance(
                event_payload, ContractParameter
        ) or event_payload.Type != ContractParameterType.Array:
            self.logger.info(
                "[invalid event_payload] SmartContract Runtime.Notify event: %s",
                event)
            return

        payload = event_payload.Value

        # Make sure that the event payload list has at least one element.
        if not len(payload):
            self.logger.info(
                "[no event_payload] SmartContract Runtime.Notify event: %s",
                event)
            return

        if event.test_mode:
            self.logger.info(
                "[test_mode] SmartContract Runtime.Notify event: %s", event)
            return

        if not event.execution_success:
            self.logger.info(
                "[execution_success=false] SmartContract Runtime.Notify event: %s",
                event)
            return

        # The event payload list has at least one element. As developer of the smart contract
        # you should know what data-type is in the bytes, and how to decode it. In this example,
        # it's just a string, so we decode it with utf-8:
        event_type = payload[0].Value.decode("utf-8")

        # Only looking for transfer events, so ignore everything else
        if event_type != 'transfer':
            return

        self.logger.info("[event_payload] Processing event: %s", event)

        # To address
        to_address = self.get_address(payload[2].Value)

        # Ignore transfers between other accounts. only care about payments to the niche payment address
        if to_address != self.niche_payment_address:
            self.logger.info("- ignoring unknown %s: to %s; not %s",
                             event_type, to_address,
                             self.niche_payment_address)
            return

        # From address & NRVE amount
        from_address = self.get_address(payload[1].Value)
        raw_nrve_amount = payload[3].Value
        # bl: there can be different data types returned in the amount payload for some reason, so detect which it is (BigInteger/int or bytes)
        if isinstance(raw_nrve_amount, int):
            nrve_amount = raw_nrve_amount
        else:
            nrve_amount = int.from_bytes(raw_nrve_amount, 'little')

        # bl: event.tx_hash is a UInt256, so convert it to a hex string
        tx_hash = event.tx_hash.ToString()

        self.process_nrve_transaction(event, event_type, from_address,
                                      nrve_amount, tx_hash)

    def process_nrve_transaction(self, event, event_type, from_address,
                                 nrve_amount, tx_hash):

        # Connect to the database
        connection = pymysql.connect(host=self.db_config['host'],
                                     user=self.db_config['user'],
                                     password=self.db_config['password'],
                                     db=self.db_config['db'],
                                     charset='utf8mb4',
                                     cursorclass=pymysql.cursors.DictCursor)

        try:
            with connection.cursor() as cursor:
                log = "- payment %s: from %s: %s NRVE (tx: %s)" % (
                    event_type, from_address, nrve_amount, tx_hash)
                self.logger.info(log)
                sql = ("select oid from `NrvePayment`\n"
                       "where fromNeoAddress = %s\n"
                       "and nrveAmount = %s\n"
                       "and paymentStatus = 0\n"
                       "and transactionId is null\n"
                       "for update;")

                args = (from_address, nrve_amount)
                cursor.execute(sql, args)

                if cursor.rowcount == 0:
                    # This could be one of two scenarios:
                    #   1. Transaction does not exist, making it invalid (Refund).
                    #   2. Transaction was processed by a different thread (transactionId has been updated to a non-null value).
                    self.handle_unknown_transaction(connection, event,
                                                    from_address, nrve_amount,
                                                    tx_hash)
                    return

                elif cursor.rowcount > 1:
                    subject = 'FATAL! Identified multiple payments by unique key. Should not be possible!'
                    self.logger.error(subject + ': %s', event)
                    self.send_email(
                        subject,
                        self.format_error_message(
                            {"Transaction Id": tx_hash},
                            {"From Address": from_address},
                            {"To Address": self.niche_payment_address},
                            {"NRVE Amount": nrve_amount},
                            {"Number of Transactions": cursor.rowcount}))
                    return

                block = self.blockchain.GetHeaderByHeight(event.block_number)

                # when a payment is outstanding, it will be recorded with the expected "from address", the proper
                # nrveAmount (in "neurons") and a paymentStatus of 0 which indicates it's pending payment
                sql = ("update `NrvePayment`\n"
                       "set transactionId = %s\n"
                       ", transactionDate = from_unixtime(%s)\n"
                       ", foundByExternalApi = 0\n"
                       "where fromNeoAddress = %s\n"
                       "and nrveAmount = %s\n"
                       "and paymentStatus = 0\n"
                       "and transactionId is null;")
                args = (tx_hash, block.Timestamp, from_address, nrve_amount)

                # Create a new record
                cursor.execute(sql, args)

                if cursor.rowcount != 1:
                    subject = 'Failed updating payment. Should not be possible since it was already locked for update'
                    self.logger.error(subject + ': %s', event)
                    self.send_email(
                        subject,
                        self.format_error_message(
                            {"Transaction Id": tx_hash},
                            {"From Address": from_address},
                            {"To Address": self.niche_payment_address},
                            {"NRVE Amount": nrve_amount}))
                    return

                self.send_email("Successful Niche Payment", log)

            # connection is not autocommit by default. So you must commit to save your changes.
            connection.commit()

        except MySQLError as e:
            error_message = 'ERROR: event %s: {!r}, errno is {}'.format(
                event, e, e.args[0])
            self.logger.error(error_message)
            self.send_email('Niche Payment Error', error_message)

        finally:
            connection.close()

    def handle_unknown_transaction(self, connection, event, from_address,
                                   nrve_amount, tx_hash):

        try:
            with connection.cursor() as cursor:
                sql = (
                    "select oid from `NrvePayment` where transactionId = %s;")
                params = tx_hash
                cursor.execute(sql, params)

                if cursor.rowcount == 0:
                    # Send refund email
                    subject = 'Failed identifying niche payment. ' + self.network_type + ' refund required!'
                    self.logger.error(subject + ': %s', event)
                    self.send_email(
                        subject,
                        self.format_error_message(
                            {"Transaction Id": tx_hash},
                            {"From Address": from_address},
                            {"To Address": self.niche_payment_address},
                            {"NRVE Amount": nrve_amount}))
                    return

                elif cursor.rowcount == 1:
                    # Transaction is valid, no need to process any further.
                    self.logger.info(
                        "Transaction %s was already processed by a different thread.",
                        tx_hash)
                    return

                else:
                    self.logger.error(
                        "FATAL! Found %s records for transaction %s. ",
                        cursor.rowcount, tx_hash)
                    return

        except MySQLError as e:
            error_message = 'ERROR: event %s: {!r}, errno is {}'.format(
                event, e, e.args[0])
            self.logger.error(error_message)
            self.send_email('Unable to verify Niche Payment status.',
                            error_message)

    def send_email(self, subject, body):
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = 'Narrative ' + self.network_type + ' <' + self.smtp_config[
            'from_address'] + '>'
        msg['To'] = self.smtp_config['to_address']

        # Send the message via our own SMTP server.
        # bl: production servers user port 587
        s = smtplib.SMTP(self.smtp_config['host'], self.smtp_config['port'])
        if self.smtp_config['use_tls']:
            s.starttls()
        if self.smtp_config['username']:
            s.login(self.smtp_config['username'], self.smtp_config['password'])
        s.send_message(msg)
        s.quit()

    @staticmethod
    def format_error_message(*args):
        message = ''
        for eachDict in args:
            for key, value in eachDict.items():
                if message != '':
                    message += "\n"
                message += str(key) + ": " + str(value)
        return message