Add "state" tests
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
6f4312cb4f
commit
14aa8a6d59
21 changed files with 116 additions and 3 deletions
0
companion/__init__.py
Normal file
0
companion/__init__.py
Normal file
14
companion/coingecko.py
Normal file
14
companion/coingecko.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_rate(ids, vs_currencies):
|
||||
logger.debug(f'getting {ids} price in {vs_currencies} on coingecko')
|
||||
url = f'https://api.coingecko.com/api/v3/simple/price?ids={ids}&vs_currencies={vs_currencies}'
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
logger.debug(r.json())
|
||||
return r.json()[ids.lower()][vs_currencies.lower()]
|
26
companion/config.py
Normal file
26
companion/config.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
from jsonschema import validate
|
||||
|
||||
absolute_path = os.path.split(os.path.abspath(__file__))[0]
|
||||
|
||||
|
||||
class InvalidConfigException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def read_config(filename=None):
|
||||
if filename and os.path.isfile(filename):
|
||||
with open(filename, 'r') as fd:
|
||||
return json.load(fd)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
if config is None:
|
||||
raise InvalidConfigException('config is not a dict')
|
||||
with open(os.path.join(absolute_path, 'config.schema.json'), 'r') as fd:
|
||||
schema = json.loads(fd.read())
|
||||
validate(instance=config, schema=schema)
|
40
companion/config.schema.json
Normal file
40
companion/config.schema.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"telegram": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat_id": {
|
||||
"type": "number"
|
||||
},
|
||||
"auth_key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"chat_id",
|
||||
"auth_key"
|
||||
]
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"miner": {
|
||||
"type": "string"
|
||||
},
|
||||
"pools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"flexpool",
|
||||
"ethermine"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"state_file": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
91
companion/main.py
Normal file
91
companion/main.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from coingecko import get_rate
|
||||
from config import read_config, validate_config
|
||||
from requests.exceptions import HTTPError
|
||||
from state import State
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_STATE_FILE = 'state.json'
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO,
|
||||
help='print more output')
|
||||
parser.add_argument('-d', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG,
|
||||
default=logging.WARNING, help='print even more output')
|
||||
parser.add_argument('-o', '--logfile', help='logging file location')
|
||||
parser.add_argument('-N', '--disable-notifications', dest='disable_notifications', action='store_true',
|
||||
help='do not send notifications')
|
||||
parser.add_argument('-c', '--config', help='configuration file name', default='config.json')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def setup_logging(args):
|
||||
log_format = '%(asctime)s %(levelname)s: %(message)s' if args.logfile else '%(levelname)s: %(message)s'
|
||||
logging.basicConfig(format=log_format, level=args.loglevel, filename=args.logfile)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_arguments()
|
||||
setup_logging(args)
|
||||
|
||||
config = read_config(args.config)
|
||||
validate_config(config)
|
||||
|
||||
state = State(filename=config.get('state_file', DEFAULT_STATE_FILE))
|
||||
|
||||
exchange_rate = None
|
||||
currency = config.get('currency')
|
||||
|
||||
notifier = None
|
||||
if config.get('telegram') and not args.disable_notifications:
|
||||
from telegram import TelegramNotifier
|
||||
notifier = TelegramNotifier(**config['telegram'])
|
||||
|
||||
if 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))
|
||||
|
||||
for pool in config.get('pools', []):
|
||||
pool_state = state.get(pool)
|
||||
|
||||
if pool == 'flexpool':
|
||||
from pools.flexpool import FlexpoolHandler
|
||||
handler = FlexpoolHandler(exchange_rate=exchange_rate, currency=currency, notifier=notifier)
|
||||
elif pool == 'ethermine':
|
||||
from pools.ethermine import EthermineHandler
|
||||
handler = EthermineHandler(exchange_rate=exchange_rate, currency=currency, notifier=notifier)
|
||||
else:
|
||||
logger.warning(f'pool {pool} not supported')
|
||||
continue
|
||||
|
||||
last_block = handler.watch_blocks(last_block=pool_state.get('block'))
|
||||
if last_block:
|
||||
logger.debug(f'saving {pool} block to state file')
|
||||
state.write(pool_name=pool, block_number=last_block)
|
||||
|
||||
if config.get('miner'):
|
||||
last_balance, last_transaction = handler.watch_miner(address=config['miner'],
|
||||
last_balance=pool_state.get('balance'),
|
||||
last_transaction=pool_state.get('payment'))
|
||||
if last_balance is not None:
|
||||
logger.debug(f'saving {pool} miner balance to state file')
|
||||
state.write(pool_name=pool, miner_balance=last_balance)
|
||||
if last_transaction:
|
||||
logger.debug(f'saving {pool} miner payment to state file')
|
||||
state.write(pool_name=pool, miner_payment=last_transaction)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
51
companion/pools/__init__.py
Normal file
51
companion/pools/__init__.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_NOTIFICATIONS_COUNT = 5
|
||||
|
||||
|
||||
class Handler:
|
||||
def __init__(self, pool_name, exchange_rate=None, currency=None, notifier=None):
|
||||
self.pool_name = pool_name
|
||||
self.exchange_rate = exchange_rate
|
||||
self.currency = currency
|
||||
self.notifier = notifier
|
||||
|
||||
def _watch_miner_balance(self, miner, last_balance=None):
|
||||
logger.debug('watching miner balance')
|
||||
if miner.raw_balance != last_balance:
|
||||
logger.info('miner balance has changed')
|
||||
if self.notifier:
|
||||
logger.debug('sending balance notification')
|
||||
arguments = {'pool': self.pool_name, 'address': miner.address, 'url': miner.url,
|
||||
'balance': miner.balance, 'balance_fiat': miner.balance_fiat,
|
||||
'balance_percentage': miner.balance_percentage}
|
||||
try:
|
||||
self.notifier.notify_balance(**arguments)
|
||||
logger.info('balance notification sent')
|
||||
except Exception as err:
|
||||
logger.error('failed to send notification')
|
||||
logger.exception(err)
|
||||
return miner.raw_balance
|
||||
|
||||
def _watch_miner_payments(self, miner, last_transaction=None):
|
||||
if miner.last_transaction and miner.last_transaction.txid != last_transaction:
|
||||
logger.debug('watching miner payments')
|
||||
# send notifications for recent payements only
|
||||
for transaction in miner.transactions[MAX_NOTIFICATIONS_COUNT:]:
|
||||
if not last_transaction or transaction.txid > last_transaction:
|
||||
logger.info(f'new payment {transaction.txid}')
|
||||
if self.notifier:
|
||||
logger.debug('sending payment notification')
|
||||
arguments = {'pool': self.pool_name, 'address': miner.address, 'txid': transaction.txid,
|
||||
'amount': transaction.amount, 'amount_fiat': transaction.amount_fiat,
|
||||
'time': transaction.time, 'duration': transaction.duration}
|
||||
try:
|
||||
self.notifier.notify_payment(**arguments)
|
||||
logger.info('payment notification sent')
|
||||
except Exception as err:
|
||||
logger.error('failed to send notification')
|
||||
logger.exception(err)
|
||||
if miner.last_transaction and miner.last_transaction.txid:
|
||||
return miner.last_transaction.txid
|
119
companion/pools/ethermine.py
Normal file
119
companion/pools/ethermine.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from ethermine import Ethermine
|
||||
from humanfriendly import format_timespan
|
||||
from pools import Handler
|
||||
from utils import convert_fiat, format_weis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
eth = Ethermine()
|
||||
|
||||
|
||||
class Miner:
|
||||
def __init__(self, address, exchange_rate=None, currency=None):
|
||||
self.address = address
|
||||
self.raw_balance = self.get_unpaid_balance(address)
|
||||
self.balance = format_weis(self.raw_balance)
|
||||
self.balance_fiat = None
|
||||
if exchange_rate and currency:
|
||||
self.balance_fiat = convert_fiat(amount=self.raw_balance, exchange_rate=exchange_rate, currency=currency)
|
||||
payout_threshold = self.get_payout_threshold(address)
|
||||
self.balance_percentage = self.format_balance_percentage(payout_threshold=payout_threshold,
|
||||
balance=self.raw_balance)
|
||||
self.transactions = self.get_payouts(address, exchange_rate, currency)
|
||||
|
||||
@staticmethod
|
||||
def get_unpaid_balance(address):
|
||||
dashboard = eth.miner_dashboard(address)
|
||||
return dashboard['currentStatistics']['unpaid']
|
||||
|
||||
@staticmethod
|
||||
def get_payouts(address, exchange_rate=None, currency=None):
|
||||
payouts = eth.miner_payouts(address)
|
||||
# convert to transactions
|
||||
transactions = []
|
||||
for payout in payouts:
|
||||
transaction = Transaction(txid=payout['txHash'], timestamp=payout['paidOn'], amount=payout['amount'],
|
||||
duration=payout['end']-payout['start'], exchange_rate=exchange_rate,
|
||||
currency=currency)
|
||||
transactions.append(transaction)
|
||||
# sort by older timestamp first
|
||||
return sorted(transactions)
|
||||
|
||||
@staticmethod
|
||||
def get_payout_threshold(address):
|
||||
return eth.miner_settings(address)['minPayout']
|
||||
|
||||
@staticmethod
|
||||
def format_balance_percentage(payout_threshold, balance):
|
||||
return f'{round(balance*100/payout_threshold, 2)}%'
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f'https://ethermine.org/miners/{self.address}/dashboard'
|
||||
|
||||
@property
|
||||
def last_transaction(self):
|
||||
if self.transactions:
|
||||
return self.transactions[-1]
|
||||
|
||||
def __repr__(self):
|
||||
attributes = {'balance': self.balance, 'raw_balance': self.raw_balance,
|
||||
'balance_percentage': self.balance_percentage, 'url': self.url}
|
||||
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})>'
|
||||
|
||||
|
||||
class Transaction:
|
||||
def __init__(self, txid, amount, timestamp, duration, exchange_rate=None, currency=None):
|
||||
self.txid = txid
|
||||
self.time = datetime.fromtimestamp(timestamp)
|
||||
self.raw_amount = amount
|
||||
self.amount = format_weis(amount)
|
||||
self.amount_fiat = None
|
||||
self.duration = format_timespan(duration)
|
||||
if exchange_rate and currency:
|
||||
self.amount_fiat = convert_fiat(amount=self.raw_amount, exchange_rate=exchange_rate, currency=currency)
|
||||
|
||||
def __lt__(self, trx):
|
||||
"""Order by datetime asc"""
|
||||
return self.time < trx.time
|
||||
|
||||
def __eq__(self, trx):
|
||||
return self.txid == trx.txid
|
||||
|
||||
def __repr__(self):
|
||||
attributes = {'time': self.time, 'amount': self.amount, 'raw_amount': self.raw_amount,
|
||||
'duration': self.duration}
|
||||
if self.amount_fiat:
|
||||
attributes['amount_fiat'] = self.amount_fiat
|
||||
formatted_attributes = ' '.join([f'{k}="{v}"' for k, v in attributes.items()])
|
||||
return f'<Transaction #{self.txid} ({formatted_attributes})>'
|
||||
|
||||
|
||||
class EthermineHandler(Handler):
|
||||
def __init__(self, exchange_rate=None, currency=None, notifier=None, pool_name='ethermine'):
|
||||
super().__init__(pool_name=pool_name, exchange_rate=exchange_rate, currency=currency, notifier=notifier)
|
||||
|
||||
def watch_blocks(self, last_block=None):
|
||||
logger.debug('not implemented yet')
|
||||
|
||||
def watch_miner(self, address, last_balance=None, last_transaction=None):
|
||||
logger.debug(f'watching miner {address}')
|
||||
try:
|
||||
miner = Miner(address=address, exchange_rate=self.exchange_rate, currency=self.currency)
|
||||
except Exception as err:
|
||||
logger.error(f'miner {address} not found')
|
||||
logger.exception(err)
|
||||
return
|
||||
|
||||
logger.debug(miner)
|
||||
|
||||
last_balance = self._watch_miner_balance(miner=miner, last_balance=last_balance)
|
||||
last_transaction = self._watch_miner_payments(miner=miner, last_transaction=last_transaction)
|
||||
|
||||
return last_balance, last_transaction
|
178
companion/pools/flexpool.py
Normal file
178
companion/pools/flexpool.py
Normal file
|
@ -0,0 +1,178 @@
|
|||
import logging
|
||||
|
||||
import flexpoolapi
|
||||
from humanfriendly import format_timespan
|
||||
from pools import MAX_NOTIFICATIONS_COUNT, Handler
|
||||
from utils import convert_fiat, convert_weis, format_weis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_BLOCKS_COUNT = 10
|
||||
MAX_PAYMENTS_COUNT = 10
|
||||
|
||||
|
||||
class Block:
|
||||
def __init__(self, number, hash, time, round_time, reward, luck, exchange_rate=None, currency=None):
|
||||
self.number = int(number)
|
||||
self.hash = hash
|
||||
self.time = time
|
||||
self.round_time = format_timespan(round_time)
|
||||
self.reward = format_weis(reward)
|
||||
self.reward_fiat = None
|
||||
if exchange_rate and currency:
|
||||
self.reward_fiat = convert_fiat(amount=reward, exchange_rate=exchange_rate, currency=currency)
|
||||
self.luck = f'{int(luck*100)}%'
|
||||
|
||||
def __lt__(self, block):
|
||||
return self.number < block.number
|
||||
|
||||
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
|
||||
miner = flexpoolapi.miner(address)
|
||||
self.raw_balance = miner.balance()
|
||||
self.balance = convert_weis(self.raw_balance)
|
||||
if exchange_rate and currency:
|
||||
self.balance_fiat = convert_fiat(amount=self.raw_balance, exchange_rate=exchange_rate, currency=currency)
|
||||
payout_threshold = self.get_payout_threshold(miner)
|
||||
self.balance_percentage = self.format_balance_percentage(payout_threshold=payout_threshold,
|
||||
balance=self.raw_balance)
|
||||
self.transactions = self.get_payements(miner)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return f'https://flexpool.io/{self.address}'
|
||||
|
||||
@staticmethod
|
||||
def get_payout_threshold(miner):
|
||||
return miner.details().min_payout_threshold
|
||||
|
||||
@staticmethod
|
||||
def format_balance_percentage(payout_threshold, balance):
|
||||
return f'{round(balance*100/payout_threshold, 2)}%'
|
||||
|
||||
@staticmethod
|
||||
def get_payements(miner, exchange_rate=None, currency=None):
|
||||
# crawl payments
|
||||
transactions = []
|
||||
payments_count = 0
|
||||
current_page = 0
|
||||
while payments_count < MAX_PAYMENTS_COUNT:
|
||||
logger.debug(f'fetching payments page {current_page}')
|
||||
page = miner.payments_paged(page=current_page)
|
||||
if not page.contents:
|
||||
break
|
||||
for payment in page.contents:
|
||||
# convert to transaction
|
||||
transaction = Transaction(txid=payment.txid, time=payment.time, amount=payment.amount,
|
||||
duration=payment.duration, exchange_rate=exchange_rate,
|
||||
currency=currency)
|
||||
transactions.append(transaction)
|
||||
payments_count += 1
|
||||
current_page += 1
|
||||
if current_page > page.total_pages:
|
||||
break
|
||||
# sort transactions from oldest to newest
|
||||
return sorted(transactions)
|
||||
|
||||
@property
|
||||
def last_transaction(self):
|
||||
if self.transactions:
|
||||
return self.transactions[-1]
|
||||
|
||||
def __repr__(self):
|
||||
attributes = {'balance': self.balance, 'raw_balance': self.raw_balance,
|
||||
'balance_percentage': self.balance_percentage, 'url': self.url}
|
||||
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})>'
|
||||
|
||||
|
||||
class Transaction:
|
||||
def __init__(self, txid, amount, time, duration, exchange_rate=None, currency=None):
|
||||
self.txid = txid
|
||||
self.time = time
|
||||
self.raw_amount = amount
|
||||
self.amount = format_weis(amount)
|
||||
self.amount_fiat = None
|
||||
self.duration = format_timespan(duration)
|
||||
if exchange_rate and currency:
|
||||
self.amount_fiat = convert_fiat(amount=self.raw_amount, exchange_rate=exchange_rate, currency=currency)
|
||||
|
||||
def __lt__(self, trx):
|
||||
return self.time < trx.time
|
||||
|
||||
def __repr__(self):
|
||||
attributes = {'time': self.time, 'amount': self.amount, 'raw_amount': self.raw_amount,
|
||||
'duration': self.duration}
|
||||
if self.amount_fiat:
|
||||
attributes['amount_fiat'] = self.amount_fiat
|
||||
formatted_attributes = ' '.join([f'{k}="{v}"' for k, v in attributes.items()])
|
||||
return f'<Transaction #{self.txid} ({formatted_attributes})>'
|
||||
|
||||
|
||||
class FlexpoolHandler(Handler):
|
||||
def __init__(self, exchange_rate=None, currency=None, notifier=None, pool_name='flexpool'):
|
||||
super().__init__(pool_name=pool_name, exchange_rate=exchange_rate, currency=currency, notifier=notifier)
|
||||
|
||||
def watch_blocks(self, last_block=None):
|
||||
logger.debug('watching last blocks')
|
||||
last_remote_block = None
|
||||
blocks = self.get_blocks(exchange_rate=self.exchange_rate, currency=self.currency)
|
||||
if blocks:
|
||||
# don't spam block notification at initialization
|
||||
for block in blocks[MAX_NOTIFICATIONS_COUNT:]:
|
||||
if not last_block or last_block < block.number:
|
||||
logger.info(f'new block {block.number}')
|
||||
if self.notifier:
|
||||
logger.debug('sending block notification')
|
||||
arguments = {'pool': self.pool_name, 'number': block.number, 'hash': block.hash,
|
||||
'reward': block.reward, 'time': block.time, 'round_time': block.round_time,
|
||||
'luck': block.luck, 'reward_fiat': block.reward_fiat}
|
||||
try:
|
||||
self.notifier.notify_block(**arguments)
|
||||
logger.info('block notification sent')
|
||||
except Exception as err:
|
||||
logger.error('failed to send notification')
|
||||
logger.exception(err)
|
||||
last_remote_block = block
|
||||
if last_remote_block and last_remote_block.number:
|
||||
return last_remote_block.number
|
||||
|
||||
@staticmethod
|
||||
def get_blocks(exchange_rate=None, currency=None):
|
||||
remote_blocks = flexpoolapi.pool.last_blocks(count=MAX_BLOCKS_COUNT)
|
||||
# convert to blocks
|
||||
blocks = []
|
||||
for remote_block in remote_blocks:
|
||||
block = Block(number=remote_block.number, hash=remote_block.hash, time=remote_block.time,
|
||||
round_time=remote_block.round_time, reward=remote_block.total_rewards, luck=remote_block.luck,
|
||||
exchange_rate=exchange_rate, currency=currency)
|
||||
blocks.append(block)
|
||||
# sort by block number
|
||||
return sorted(blocks)
|
||||
|
||||
def watch_miner(self, address, last_balance=None, last_transaction=None):
|
||||
logger.debug(f'watching miner {address}')
|
||||
try:
|
||||
miner = Miner(address=address, exchange_rate=self.exchange_rate, currency=self.currency)
|
||||
except Exception as err:
|
||||
logger.error(f'miner {address} not found')
|
||||
logger.exception(err)
|
||||
return
|
||||
|
||||
logger.debug(miner)
|
||||
|
||||
last_balance = self._watch_miner_balance(miner=miner, last_balance=last_balance)
|
||||
last_transaction = self._watch_miner_payments(miner=miner, last_transaction=last_transaction)
|
||||
|
||||
return last_balance, last_transaction
|
34
companion/state.py
Normal file
34
companion/state.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
|
||||
class State:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.create()
|
||||
|
||||
def create(self):
|
||||
if not os.path.isfile(self.filename):
|
||||
with open(self.filename, 'w') as fd:
|
||||
json.dump({}, fd)
|
||||
|
||||
def read(self):
|
||||
with open(self.filename, 'r') as fd:
|
||||
return json.load(fd)
|
||||
|
||||
def write(self, pool_name, block_number=None, miner_balance=None, miner_payment=None):
|
||||
content = self.read()
|
||||
if pool_name not in content:
|
||||
content[pool_name] = {}
|
||||
if block_number is not None:
|
||||
content[pool_name]['block'] = block_number
|
||||
if miner_balance is not None:
|
||||
content[pool_name]['balance'] = miner_balance
|
||||
if miner_payment:
|
||||
content[pool_name]['payment'] = miner_payment
|
||||
with open(self.filename, 'w') as fd:
|
||||
json.dump(content, fd, indent=2, separators=(',', ': '))
|
||||
|
||||
def get(self, key):
|
||||
content = self.read()
|
||||
return content.get(key, {})
|
66
companion/telegram.py
Normal file
66
companion/telegram.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
import logging
|
||||
import os
|
||||
from copy import copy
|
||||
|
||||
import requests
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
absolute_path = os.path.split(os.path.abspath(__file__))[0]
|
||||
|
||||
|
||||
class TelegramNotifier:
|
||||
def __init__(self, chat_id, auth_key):
|
||||
self._auth_key = auth_key
|
||||
self._default_payload = {'auth_key': auth_key, 'chat_id': chat_id, 'parse_mode': 'MarkdownV2',
|
||||
'disable_web_page_preview': True}
|
||||
|
||||
@staticmethod
|
||||
def _markdown_escape(text):
|
||||
text = str(text)
|
||||
for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']:
|
||||
text = text.replace(special_char, fr'\{special_char}')
|
||||
return text
|
||||
|
||||
def _generate_payload(self, message_variables, template_name):
|
||||
payload = copy(self._default_payload)
|
||||
template_path = os.path.join(absolute_path, 'templates')
|
||||
loader = FileSystemLoader(template_path)
|
||||
env = Environment(loader=loader)
|
||||
template = env.get_template(template_name)
|
||||
template_variables = {}
|
||||
for key, value in message_variables.items():
|
||||
template_variables[key] = self._markdown_escape(value)
|
||||
text = template.render(**template_variables)
|
||||
payload['text'] = text
|
||||
return payload
|
||||
|
||||
def notify_block(self, pool, number, hash, reward, time, round_time, luck, reward_fiat=None):
|
||||
message_variables = {'pool': pool, 'number': number, 'hash': hash, 'reward': reward, 'time': time,
|
||||
'round_time': round_time, 'luck': luck, 'reward_fiat': reward_fiat}
|
||||
payload = self._generate_payload(message_variables, 'block.md.j2')
|
||||
self._send_message(payload)
|
||||
|
||||
def notify_balance(self, pool, address, url, balance, balance_percentage, balance_fiat=None):
|
||||
message_variables = {'pool': pool, 'address': address, 'url': url, 'balance': balance,
|
||||
'balance_percentage': balance_percentage, 'balance_fiat': balance_fiat}
|
||||
payload = self._generate_payload(message_variables, 'balance.md.j2')
|
||||
self._send_message(payload)
|
||||
|
||||
def notify_payment(self, pool, address, txid, amount, time, duration, amount_fiat=None):
|
||||
message_variables = {'pool': pool, 'address': address, 'txid': txid, 'amount': amount,
|
||||
'amount_fiat': amount_fiat, 'time': time, 'duration': duration}
|
||||
payload = self._generate_payload(message_variables, 'payment.md.j2')
|
||||
self._send_message(payload)
|
||||
|
||||
def _send_message(self, payload):
|
||||
logger.debug(self._sanitize(payload))
|
||||
r = requests.post(f'https://api.telegram.org/bot{self._auth_key}/sendMessage', json=payload)
|
||||
r.raise_for_status()
|
||||
|
||||
@staticmethod
|
||||
def _sanitize(payload):
|
||||
payload = copy(payload)
|
||||
if 'auth_key' in payload:
|
||||
payload['auth_key'] = '***'
|
||||
return payload
|
5
companion/templates/balance.md.j2
Normal file
5
companion/templates/balance.md.j2
Normal file
|
@ -0,0 +1,5 @@
|
|||
*💰 New {{pool}} balance*
|
||||
|
||||
*Address*: [{{address}}]({{url}})
|
||||
*Unpaid balance*: {{balance}} {% if balance_fiat != 'None' %}\({{balance_fiat}}\){% endif %}
|
||||
*Unpaid percentage*: {{balance_percentage}}
|
7
companion/templates/block.md.j2
Normal file
7
companion/templates/block.md.j2
Normal file
|
@ -0,0 +1,7 @@
|
|||
*⛏️ New {{pool}} block*
|
||||
|
||||
*Number*: [{{number}}](https://etherscan.io/block/{{hash}})
|
||||
*Reward*: {{reward}} {% if reward_fiat != 'None' %}\({{reward_fiat}}\){% endif %}
|
||||
*Date/Time*: {{time}}
|
||||
*Round time*: {{round_time}}
|
||||
*Luck*: {{luck}}
|
7
companion/templates/payment.md.j2
Normal file
7
companion/templates/payment.md.j2
Normal file
|
@ -0,0 +1,7 @@
|
|||
*💵 New {{pool}} payment*
|
||||
|
||||
*Amount*: {{amount}} {% if amount_fiat != 'None' %}\({{amount_fiat}}\){% endif %}
|
||||
*ID*: [{{txid}}](https://etherscan.io/tx/{{txid}})
|
||||
*Address*: [{{address}}](https://flexpool.io/{{address}})
|
||||
*Date/Time*: {{time}}
|
||||
*Duration*: {{duration}}
|
15
companion/utils.py
Normal file
15
companion/utils.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
def convert_weis(weis, precision=5):
|
||||
return round(weis / 10**18, precision)
|
||||
|
||||
|
||||
def format_weis(weis, precision=5):
|
||||
amount = convert_weis(weis=weis, precision=precision)
|
||||
if amount == int(amount):
|
||||
amount = int(amount)
|
||||
return f'{amount} ETH'
|
||||
|
||||
|
||||
def convert_fiat(amount, exchange_rate, currency):
|
||||
converted = round(convert_weis(amount)*exchange_rate, 2)
|
||||
converted = f'{converted} {currency}'
|
||||
return converted
|
Reference in a new issue