diff --git a/flexpool.py b/flexpool.py new file mode 100644 index 0000000..c7a089e --- /dev/null +++ b/flexpool.py @@ -0,0 +1,78 @@ +import flexpoolapi +from flexpoolapi.utils import format_weis +from humanfriendly import format_timespan + + +class BlockNotFoundException(Exception): + pass + + +def convert_weis(weis, prec=5): + return round(weis / 10**18, prec) + + +class LastBlock: + def __init__(self, exchange_rate=None, currency=None): + self._exchange_rate = exchange_rate + self._currency = currency + block = self.get_last_block() + self.number = block.number + self.time = block.time + self.raw_reward = block.total_rewards + self.reward = format_weis(block.total_rewards) + self.reward_fiat = self.convert_reward() + self.round_time = format_timespan(block.round_time) + self.luck = f'{int(block.luck*100)}%' + + @staticmethod + def get_last_block(): + block = flexpoolapi.pool.last_blocks(count=1)[0] + if not block: + raise BlockNotFoundException('No block found') + return block + + def convert_reward(self): + if self._exchange_rate and self._currency: + converted = round(convert_weis(self.raw_reward)*self._exchange_rate, 2) + converted = f'{converted} {self._currency}' + return converted + + def __repr__(self): + attributes = {'time': self.time, 'reward': self.reward, 'round_time': self.round_time, 'luck': self.luck} + if self.reward_fiat: + attributes['reward_fiat'] = self.reward_fiat + formatted_attributes = ' '.join([f'{k}="{v}"' for k, v in attributes.items()]) + return f'' + + +class Miner: + def __init__(self, address, exchange_rate=None, currency=None): + self.address = address + self._exchange_rate = exchange_rate + self._currency = currency + miner = flexpoolapi.miner(address) + self.raw_balance = miner.balance() + self.details = miner.details() + self.balance = self.format_balance() + self.balance_fiat = self.convert_balance() + self.balance_percentage = self.format_balance_percentage() + + def format_balance(self): + return format_weis(self.raw_balance) + + def format_balance_percentage(self): + return f'{round(self.raw_balance*100/self.details.min_payout_threshold, 2)}%' + + def convert_balance(self): + if self._exchange_rate and self._currency: + converted = round(convert_weis(self.raw_balance)*self._exchange_rate, 2) + converted = f'{converted} {self._currency}' + return converted + + def __repr__(self): + attributes = {'balance': self.balance, 'raw_balance': self.raw_balance, + 'balance_percentage': self.balance_percentage} + if self.balance_fiat: + attributes['balance_fiat'] = self.balance_fiat + formatted_attributes = ' '.join([f'{k}="{v}"' for k, v in attributes.items()]) + return f'' diff --git a/main.py b/main.py index 3ebd28b..232f96a 100644 --- a/main.py +++ b/main.py @@ -1,20 +1,21 @@ #!/usr/bin/env python3 import argparse import logging -import os -import flexpoolapi import telegram from coingecko import get_rate from config import read_config, validate_config -from flexpoolapi.utils import format_weis -from humanfriendly import format_timespan +from flexpool import BlockNotFoundException, LastBlock, Miner from requests.exceptions import HTTPError from state import read_state, write_state logger = logging.getLogger(__name__) +DEFAULT_STATE_FILE = 'state.json' +DEFAULT_CURRENCY = 'USD' + + def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, @@ -34,79 +35,29 @@ def setup_logging(args): logging.basicConfig(format=log_format, level=args.loglevel, filename=args.logfile) -def convert_weis(weis, prec=5): - return round(weis / 10**18, prec) - - -def main(): - args = parse_arguments() - setup_logging(args) - - config = read_config(args.config) - validate_config(config) - state_file = config.get('state_file', 'state.json') - currency = config.get('currency', 'USD') - +def watch_block(state_file, config, disable_notifications, exchange_rate=None, currency=None): logger.debug('fetching last mined block') - block = flexpoolapi.pool.last_blocks(count=1)[0] - - block_number = block.number - block_time = block.time - block_reward = format_weis(block.total_rewards) - block_round_time = format_timespan(block.round_time) - block_luck = f'{int(block.luck*100)}%' - - if not block: - logger.info('no block found') - return - - if not os.path.isfile(state_file): - logger.debug('creating state file') - write_state(filename=state_file, block_number=block.number) + try: + block = LastBlock(exchange_rate=exchange_rate, currency=currency) + except BlockNotFoundException: + logger.warning('last block found') return logger.debug('reading state file') state = read_state(filename=state_file) - if block.number != state['block']: + if block.number != state.get('block'): logger.info(f'block {block.number} mined') + logger.debug(block) - logger.debug(f'block time: {block_time}') - logger.debug(f'block reward: {block_reward}') - logger.debug(f'block round time: {block_round_time}') - logger.debug(f'block luck: {block_luck}') - - logger.debug('fetching miner details') - - miner = flexpoolapi.miner(config['miner']) - miner_balance = miner.balance() - payout_threshold = miner.details().min_payout_threshold - balance_percentage = f'{round(miner_balance*100/payout_threshold, 2)}%' - - logger.debug(f'miner unpaid balance: {format_weis(miner_balance)} ({balance_percentage})') - - logger.debug('fetching current rate') - try: - rate = get_rate(ids='ethereum', vs_currencies=currency) - except HTTPError as err: - logger.error(f'failed to get ETH/{currency} rate') - logger.debug(str(err)) - return - - balance_converted = round(convert_weis(miner_balance)*rate, 2) - logger.debug(f'miner unpaid balance converted: {balance_converted} {currency}') - - if not args.disable_notifications and config.get('telegram'): - logger.debug('sending telegram notification') - variables = {'block_number': block_number, 'block_time': block_time, 'block_reward': block_reward, - 'block_round_time': block_round_time, 'block_luck': block_luck, - 'miner_address': config['miner'], 'miner_balance': format_weis(miner_balance), - 'miner_balance_converted': balance_converted, 'miner_balance_percentage': balance_percentage, - 'currency': currency} - payload = telegram.generate_payload(chat_id=config['telegram']['chat_id'], message_variables=variables) + if not disable_notifications and config.get('telegram'): + logger.debug('sending block notification to telegram') + variables = {'number': block.number, 'time': block.time, 'reward': block.reward, + 'reward_fiat': block.reward_fiat, 'round_time': block.round_time, 'luck': block.luck} + payload = telegram.create_block_payload(chat_id=config['telegram']['chat_id'], message_variables=variables) try: telegram.send_message(auth_key=config['telegram']['auth_key'], payload=payload) - logger.info('notification sent to telegram') + logger.info('block notification sent to telegram') except HTTPError as err: logger.error('failed to send notification to telegram') logger.debug(str(err)) @@ -115,5 +66,61 @@ def main(): write_state(filename=state_file, block_number=block.number) +def watch_miner(address, state_file, config, disable_notifications, exchange_rate=None, currency=None): + logger.debug(f'watching miner {address}') + try: + miner = Miner(address=address, exchange_rate=exchange_rate, currency=currency) + except Exception as err: + logger.error('failed to find miner') + logger.debug(str(err)) + return + + logger.debug(miner) + + logger.debug('reading state file') + state = read_state(filename=state_file) + + # watch balance + if miner.raw_balance != state.get('balance'): + logger.info(f'miner {address} balance has changed') + if not disable_notifications and config.get('telegram'): + logger.debug('sending balance notification to telegram') + variables = {'address': address, 'balance': miner.balance, 'balance_fiat': miner.balance_fiat, + 'balance_percentage': miner.balance_percentage} + payload = telegram.create_balance_payload(chat_id=config['telegram']['chat_id'], + message_variables=variables) + try: + telegram.send_message(auth_key=config['telegram']['auth_key'], payload=payload) + logger.info('block notification sent to telegram') + except HTTPError as err: + logger.error('failed to send notification to telegram') + logger.debug(str(err)) + + logger.debug('writing balance to state file') + write_state(filename=state_file, miner_balance=miner.raw_balance) + + +def main(): + args = parse_arguments() + setup_logging(args) + + config = read_config(args.config) + validate_config(config) + state_file = config.get('state_file', DEFAULT_STATE_FILE) + + exchange_rate = None + currency = config.get('currency', DEFAULT_CURRENCY) + + logger.debug('fetching current rate') + try: + exchange_rate = get_rate(ids='ethereum', vs_currencies=currency) + except HTTPError as err: + logger.warning(f'failed to get ETH/{currency} rate') + logger.debug(str(err)) + + watch_block(state_file, config, args.disable_notifications, exchange_rate, currency) + watch_miner(config['miner'], state_file, config, args.disable_notifications, exchange_rate, currency) + + if __name__ == '__main__': main() diff --git a/message.md.j2 b/message.md.j2 deleted file mode 100644 index 62ef328..0000000 --- a/message.md.j2 +++ /dev/null @@ -1,11 +0,0 @@ -*⛏️ Block {{block_number}} mined by Flexpool* - -*Date/Time*: {{block_time}} -*Reward*: {{block_reward}} -*Round time*: {{block_round_time}} -*Luck*: {{block_luck}} - -💰 {{miner_address}} - -*Unpaid balance*: {{miner_balance}} \({{miner_balance_converted}} {{currency}}\) -*Unpaid percentage*: {{miner_balance_percentage}} diff --git a/state.py b/state.py index 8c8b400..a653e44 100644 --- a/state.py +++ b/state.py @@ -1,8 +1,18 @@ import json +import os -def write_state(filename, block_number): - data = {'block': block_number} +def write_state(filename, block_number=None, miner_balance=None): + data = {} + if os.path.isfile(filename): + data = read_state(filename) + + if block_number: + data['block'] = block_number + + if miner_balance: + data['balance'] = miner_balance + with open(filename, 'w') as fd: json.dump(data, fd) diff --git a/telegram.py b/telegram.py index 51a0125..a9599cd 100644 --- a/telegram.py +++ b/telegram.py @@ -15,12 +15,17 @@ def markdown_escape(text): return text -def generate_payload(chat_id, message_variables, template_file_name=None): +def create_block_payload(chat_id, message_variables): + return generate_payload(chat_id, message_variables, 'block.md.j2') + + +def create_balance_payload(chat_id, message_variables): + return generate_payload(chat_id, message_variables, 'balance.md.j2') + + +def generate_payload(chat_id, message_variables, template_name): payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} - if not template_file_name: - template_file_name = os.path.join(absolute_path, 'message.md.j2') - template_path = os.path.dirname(os.path.abspath(template_file_name)) - template_name = os.path.basename(os.path.abspath(template_file_name)) + template_path = os.path.join(absolute_path, 'templates') loader = FileSystemLoader(template_path) env = Environment(loader=loader) template = env.get_template(template_name) diff --git a/templates/balance.md.j2 b/templates/balance.md.j2 new file mode 100644 index 0000000..07030f7 --- /dev/null +++ b/templates/balance.md.j2 @@ -0,0 +1,5 @@ +*💰 New balance* + +*Address*: {{address}} +*Unpaid balance*: {{balance}} {% if balance_fiat %}\({{balance_fiat}}\){% endif %} +*Unpaid percentage*: {{balance_percentage}} diff --git a/templates/block.md.j2 b/templates/block.md.j2 new file mode 100644 index 0000000..91459da --- /dev/null +++ b/templates/block.md.j2 @@ -0,0 +1,6 @@ +*⛏️ New block {{number}}* + +*Date/Time*: {{time}} +*Reward*: {{reward}} {% if reward_fiat %}\({{reward_fiat}}\){% endif %} +*Round time*: {{round_time}} +*Luck*: {{luck}}