1
0
Fork 0

Rename tool, add ethermine and refactorization

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2021-01-27 17:44:55 +01:00
parent 59407a953e
commit d05d3816b8
No known key found for this signature in database
GPG key ID: FF42D23B580C89F7
18 changed files with 585 additions and 304 deletions

View file

@ -1,8 +1,19 @@
# flexpool-activity
# mining-companion
[Flexpool.io](https://flexpool.io) is a next-generation [Ethereum](https://ethereum.org/en/) mining pool known for their
[#STOPEIP1559](https://stopeip1559.org/) move. `flexpool-activity` is able to listen and notify when a new **block** is
mined by the pool, display the up-to-date **miner balance** and convert it to **fiat**.
Cryptocurrency mining interest has raised recently due to high [Ethereum](https://ethereum.org/en/) profitability. You
can opt for the solo-mining path or use a **mining pool** to increase your chances to receive block rewards.
`mining-companion` is able to listen and notify for the following events:
* new **block** is mined by the mining pool
* unpaid **balance** is updated
* new **payment** has been sent by the mining pool
Notifications are sent via [Telegram Messenger](https://telegram.org/).
## Supported pools
* [Ethermine](https://ethermine.org/)
* [Flexpool.io](https://flexpool.io)
## Installation
@ -10,6 +21,20 @@ mined by the pool, display the up-to-date **miner balance** and convert it to **
sudo apt install python3-virtualenv
virtualenv venv
source venv/bin/activate
```
Pool libraries are loaded at execution time. For example, if you use only "flexpool" mining pool, you don't need to
install "ethermine" libraries. Requirements files have been splitted to install only libraries you need.
```
pip install -r requirements/base.txt
pip install -r requirements/ethermine.txt
pip install -r requirements/flexpool.txt
```
To install all libraries at once:
```
pip install -r requirements.txt
```
@ -21,15 +46,17 @@ bot. You'll need the `chat_id` and `auth_key` for the next section.
## Configuration
Configuration file use the JSON format with the following keys:
* `miner`: wallet address of the miner (optional)
* `currency`: symbol of the currency to convert (optional)
* `telegram`: send notifications with Telegram (optional)
* `pools`: list of mining pools
* `miner`: wallet address of the miner
* `currency`: symbol of the currency to convert
* `telegram`: send notifications with Telegram
* `auth_key`: Telegram authentication key for the bot API
* `chat_id`: Telegram chat room id (where to send the message)
* `state_file`: persist data between runs into this file (default: `state.json`)
See [configuration example](config.example.json).
All options are optional (but the companion would do nothing).
## Usage

View file

@ -1,5 +1,9 @@
{
"miner": "string",
"pools": [
"ethermine",
"flexpool"
],
"miner": "your.eth.address",
"currency": "USD",
"telegram": {
"chat_id": 123,

View file

@ -22,6 +22,17 @@
"miner": {
"type": "string"
},
"pools": {
"type": "array",
"items": {
"type": "string",
"enum": [
"flexpool",
"ethermine"
]
},
"uniqueItems": true
},
"state_file": {
"type": "string"
}

View file

@ -1,113 +0,0 @@
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.hash = block.hash
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()
last_transactions = miner.payments_paged(page=0)
if last_transactions:
trx = last_transactions[0]
self.last_transaction = Transaction(amount=trx.amount, time=trx.time, duration=trx.duration, txid=trx.txid,
exchange_rate=exchange_rate, currency=currency)
else:
self.last_transaction = None
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})>'
class Transaction:
def __init__(self, amount, time, duration, txid, exchange_rate=None, currency=None):
self._exchange_rate = exchange_rate
self._currency = currency
self.raw_amount = amount
self.amount = format_weis(amount)
self.amount_fiat = self.convert_amount()
self.duration = format_timespan(duration)
self.txid = txid
self.time = time
def convert_amount(self):
if self._exchange_rate and self._currency:
converted = round(convert_weis(self.raw_amount)*self._exchange_rate, 2)
converted = f'{converted} {self._currency}'
return converted
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})>'

130
main.py
View file

@ -2,12 +2,10 @@
import argparse
import logging
import telegram
from coingecko import get_rate
from config import read_config, validate_config
from flexpool import BlockNotFoundException, LastBlock, Miner
from requests.exceptions import HTTPError
from state import create_state, read_state, write_state
from state import State
logger = logging.getLogger(__name__)
@ -34,81 +32,6 @@ def setup_logging(args):
logging.basicConfig(format=log_format, level=args.loglevel, filename=args.logfile)
def watch_block(config, disable_notifications, last_block=None, exchange_rate=None, currency=None):
logger.debug('fetching last mined block')
try:
block = LastBlock(exchange_rate=exchange_rate, currency=currency)
except BlockNotFoundException:
logger.warning('last block found')
return
if block.number != last_block:
logger.info(f'block {block.number} mined')
logger.debug(block)
if not disable_notifications and config.get('telegram'):
logger.debug('sending block notification to telegram')
variables = {'hash': block.hash, '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('block notification sent to telegram')
except HTTPError as err:
logger.error('failed to send notification to telegram')
logger.exception(err)
return block
def watch_miner(address, config, disable_notifications, last_balance=None, last_transaction=None, 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.exception(err)
return
logger.debug(miner)
logger.debug('watching miner balance')
if miner.raw_balance != last_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('balance notification sent to telegram')
except HTTPError as err:
logger.error('failed to send notification to telegram')
logger.debug(str(err))
logger.debug('watching miner payments')
if miner.last_transaction and miner.last_transaction.txid != last_transaction:
logger.info(f'new payment for miner {address}')
if not disable_notifications and config.get('telegram'):
logger.debug('sending payment notification to telegram')
variables = {'address': address, 'txid': miner.last_transaction.txid,
'amount': miner.last_transaction.amount, 'amount_fiat': miner.last_transaction.amount_fiat,
'time': miner.last_transaction.time, 'duration': miner.last_transaction.duration}
payload = telegram.create_payment_payload(chat_id=config['telegram']['chat_id'],
message_variables=variables)
try:
telegram.send_message(auth_key=config['telegram']['auth_key'], payload=payload)
logger.info('payment notification sent to telegram')
except HTTPError as err:
logger.error('failed to send notification to telegram')
logger.debug(str(err))
return miner
def main():
args = parse_arguments()
setup_logging(args)
@ -116,13 +39,16 @@ def main():
config = read_config(args.config)
validate_config(config)
state_file = config.get('state_file', DEFAULT_STATE_FILE)
create_state(state_file)
state = read_state(state_file)
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:
@ -131,22 +57,34 @@ def main():
logger.warning(f'failed to get ETH/{currency} rate')
logger.debug(str(err))
block = watch_block(last_block=state.get('block'), config=config, disable_notifications=args.disable_notifications,
exchange_rate=exchange_rate, currency=currency)
if block:
logger.debug('saving block number to state file')
write_state(state_file, block_number=block.number)
for pool in config.get('pools', []):
pool_state = state.get(pool)
if config.get('miner'):
miner = watch_miner(last_balance=state.get('balance'), last_transaction=state.get('payment'),
address=config['miner'], config=config, disable_notifications=args.disable_notifications,
exchange_rate=exchange_rate, currency=currency)
if miner:
logger.debug('saving miner balance to state file')
write_state(state_file, miner_balance=miner.raw_balance)
if miner.last_transaction and miner.last_transaction.txid:
logger.debug('saving miner payment to state file')
write_state(state_file, miner_payment=miner.last_transaction.txid)
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:
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__':

51
pools/__init__.py Normal file
View 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
pools/ethermine.py Normal file
View 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
pools/flexpool.py Normal file
View 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

View file

@ -1,31 +1,3 @@
appdirs==1.4.3
attrs==20.3.0
CacheControl==0.12.6
certifi==2019.11.28
chardet==3.0.4
colorama==0.4.3
contextlib2==0.6.0
distlib==0.3.0
distro==1.4.0
flexpoolapi==1.2.7.post2
html5lib==1.0.1
humanfriendly==9.1
idna==2.8
ipaddr==2.2.0
Jinja2==2.11.2
jsonschema==3.2.0
lockfile==0.12.2
MarkupSafe==1.1.1
msgpack==0.6.2
packaging==20.3
pep517==0.8.2
progress==1.5
pyparsing==2.4.6
pyrsistent==0.17.3
pytoml==0.1.21
requests==2.22.0
retrying==1.3.3
si-prefix==1.2.2
six==1.14.0
urllib3==1.25.8
webencodings==0.5.1
-r requirements/base.txt
-r requirements/ethermine.txt
-r requirements/flexpool.txt

29
requirements/base.txt Normal file
View file

@ -0,0 +1,29 @@
appdirs==1.4.4
attrs==20.3.0
CacheControl==0.12.6
certifi==2020.12.5
chardet==4.0.0
colorama==0.4.3
contextlib2==0.6.0
distlib==0.3.1
distro==1.4.0
html5lib==1.0.1
humanfriendly==9.1
idna==2.10
ipaddr==2.2.0
Jinja2==2.11.2
jsonschema==3.2.0
lockfile==0.12.2
MarkupSafe==1.1.1
msgpack==0.6.2
packaging==20.3
pep517==0.8.2
progress==1.5
pyparsing==2.4.6
pyrsistent==0.17.3
pytoml==0.1.21
requests==2.25.1
retrying==1.3.3
six==1.15.0
urllib3==1.26.3
webencodings==0.5.1

View file

@ -0,0 +1,9 @@
cfgv==3.2.0
ethermine==0.2.0
filelock==3.0.12
identify==1.5.10
nodeenv==1.5.0
pre-commit==2.9.2
PyYAML==5.3.1
toml==0.10.2
virtualenv==20.2.1

18
requirements/flexpool.txt Normal file
View file

@ -0,0 +1,18 @@
appdirs==1.4.4
certifi==2020.12.5
cfgv==3.2.0
chardet==4.0.0
distlib==0.3.1
filelock==3.0.12
flexpoolapi==1.2.7.post2
identify==1.5.10
idna==2.10
nodeenv==1.5.0
pre-commit==2.9.2
PyYAML==5.3.1
requests==2.25.1
si-prefix==1.2.2
six==1.15.0
toml==0.10.2
urllib3==1.26.3
virtualenv==20.2.1

View file

@ -2,29 +2,33 @@ import json
import os
def read_state(filename):
with open(filename, 'r') as fd:
return json.load(fd)
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 write_state(filename, block_number=None, miner_balance=None, miner_payment=None):
data = {}
if os.path.isfile(filename):
data = read_state(filename)
def read(self):
with open(self.filename, 'r') as fd:
return json.load(fd)
if block_number:
data['block'] = block_number
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:
content[pool_name]['block'] = block_number
if miner_balance:
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=(',', ': '))
if miner_balance:
data['balance'] = miner_balance
if miner_payment:
data['payment'] = miner_payment
with open(filename, 'w') as fd:
json.dump(data, fd)
def create_state(filename):
if not os.path.isfile(filename):
write_state(filename)
def get(self, key):
content = self.read()
return content.get(key, {})

View file

@ -1,5 +1,6 @@
import logging
import os
from copy import copy
import requests
from jinja2 import Environment, FileSystemLoader
@ -8,40 +9,58 @@ logger = logging.getLogger(__name__)
absolute_path = os.path.split(os.path.abspath(__file__))[0]
def markdown_escape(text):
text = str(text)
for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']:
text = text.replace(special_char, fr'\{special_char}')
return text
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 create_block_payload(chat_id, message_variables):
return generate_payload(chat_id, message_variables, 'block.md.j2')
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, '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 create_balance_payload(chat_id, message_variables):
return generate_payload(chat_id, message_variables, 'balance.md.j2')
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 create_payment_payload(chat_id, message_variables):
return generate_payload(chat_id, message_variables, 'payment.md.j2')
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()
def generate_payload(chat_id, message_variables, template_name):
payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2', 'disable_web_page_preview': True}
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] = markdown_escape(value)
text = template.render(**template_variables)
payload['text'] = text
return payload
def send_message(auth_key, payload):
logger.debug(payload)
r = requests.post(f'https://api.telegram.org/bot{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

View file

@ -1,5 +1,5 @@
*💰 New balance*
*💰 New {{pool}} balance*
*Address*: [{{address}}](https://flexpool.io/{{address}})
*Address*: [{{address}}]({{url}})
*Unpaid balance*: {{balance}} {% if balance_fiat != 'None' %}\({{balance_fiat}}\){% endif %}
*Unpaid percentage*: {{balance_percentage}}

View file

@ -1,4 +1,4 @@
*⛏️ New block*
*⛏️ New {{pool}} block*
*Number*: [{{number}}](https://etherscan.io/block/{{hash}})
*Reward*: {{reward}} {% if reward_fiat != 'None' %}\({{reward_fiat}}\){% endif %}

View file

@ -1,4 +1,4 @@
*💵 New payment*
*💵 New {{pool}} payment*
*Amount*: {{amount}} {% if amount_fiat != 'None' %}\({{amount_fiat}}\){% endif %}
*ID*: [{{txid}}](https://etherscan.io/tx/{{txid}})

15
utils.py Normal file
View 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