Separate block and balance notifications
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
75f75eea00
commit
a99d9795b3
7 changed files with 185 additions and 85 deletions
78
flexpool.py
Normal file
78
flexpool.py
Normal file
|
@ -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'<Block #{self.number} ({formatted_attributes})>'
|
||||||
|
|
||||||
|
|
||||||
|
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'<Miner #{self.address} ({formatted_attributes})>'
|
141
main.py
141
main.py
|
@ -1,20 +1,21 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
import flexpoolapi
|
|
||||||
import telegram
|
import telegram
|
||||||
from coingecko import get_rate
|
from coingecko import get_rate
|
||||||
from config import read_config, validate_config
|
from config import read_config, validate_config
|
||||||
from flexpoolapi.utils import format_weis
|
from flexpool import BlockNotFoundException, LastBlock, Miner
|
||||||
from humanfriendly import format_timespan
|
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from state import read_state, write_state
|
from state import read_state, write_state
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_STATE_FILE = 'state.json'
|
||||||
|
DEFAULT_CURRENCY = 'USD'
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO,
|
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)
|
logging.basicConfig(format=log_format, level=args.loglevel, filename=args.logfile)
|
||||||
|
|
||||||
|
|
||||||
def convert_weis(weis, prec=5):
|
def watch_block(state_file, config, disable_notifications, exchange_rate=None, currency=None):
|
||||||
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')
|
|
||||||
|
|
||||||
logger.debug('fetching last mined block')
|
logger.debug('fetching last mined block')
|
||||||
block = flexpoolapi.pool.last_blocks(count=1)[0]
|
try:
|
||||||
|
block = LastBlock(exchange_rate=exchange_rate, currency=currency)
|
||||||
block_number = block.number
|
except BlockNotFoundException:
|
||||||
block_time = block.time
|
logger.warning('last block found')
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug('reading state file')
|
logger.debug('reading state file')
|
||||||
state = read_state(filename=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.info(f'block {block.number} mined')
|
||||||
|
logger.debug(block)
|
||||||
|
|
||||||
logger.debug(f'block time: {block_time}')
|
if not disable_notifications and config.get('telegram'):
|
||||||
logger.debug(f'block reward: {block_reward}')
|
logger.debug('sending block notification to telegram')
|
||||||
logger.debug(f'block round time: {block_round_time}')
|
variables = {'number': block.number, 'time': block.time, 'reward': block.reward,
|
||||||
logger.debug(f'block luck: {block_luck}')
|
'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)
|
||||||
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)
|
|
||||||
try:
|
try:
|
||||||
telegram.send_message(auth_key=config['telegram']['auth_key'], payload=payload)
|
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:
|
except HTTPError as err:
|
||||||
logger.error('failed to send notification to telegram')
|
logger.error('failed to send notification to telegram')
|
||||||
logger.debug(str(err))
|
logger.debug(str(err))
|
||||||
|
@ -115,5 +66,61 @@ def main():
|
||||||
write_state(filename=state_file, block_number=block.number)
|
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__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -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}}
|
|
14
state.py
14
state.py
|
@ -1,8 +1,18 @@
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
def write_state(filename, block_number):
|
def write_state(filename, block_number=None, miner_balance=None):
|
||||||
data = {'block': block_number}
|
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:
|
with open(filename, 'w') as fd:
|
||||||
json.dump(data, fd)
|
json.dump(data, fd)
|
||||||
|
|
||||||
|
|
15
telegram.py
15
telegram.py
|
@ -15,12 +15,17 @@ def markdown_escape(text):
|
||||||
return 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'}
|
payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'}
|
||||||
if not template_file_name:
|
template_path = os.path.join(absolute_path, 'templates')
|
||||||
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))
|
|
||||||
loader = FileSystemLoader(template_path)
|
loader = FileSystemLoader(template_path)
|
||||||
env = Environment(loader=loader)
|
env = Environment(loader=loader)
|
||||||
template = env.get_template(template_name)
|
template = env.get_template(template_name)
|
||||||
|
|
5
templates/balance.md.j2
Normal file
5
templates/balance.md.j2
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
*💰 New balance*
|
||||||
|
|
||||||
|
*Address*: {{address}}
|
||||||
|
*Unpaid balance*: {{balance}} {% if balance_fiat %}\({{balance_fiat}}\){% endif %}
|
||||||
|
*Unpaid percentage*: {{balance_percentage}}
|
6
templates/block.md.j2
Normal file
6
templates/block.md.j2
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
*⛏️ New block {{number}}*
|
||||||
|
|
||||||
|
*Date/Time*: {{time}}
|
||||||
|
*Reward*: {{reward}} {% if reward_fiat %}\({{reward_fiat}}\){% endif %}
|
||||||
|
*Round time*: {{round_time}}
|
||||||
|
*Luck*: {{luck}}
|
Reference in a new issue