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
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()
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