Rename tool, add ethermine and refactorization
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
59407a953e
commit
d05d3816b8
18 changed files with 585 additions and 304 deletions
41
README.md
41
README.md
|
@ -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
|
Cryptocurrency mining interest has raised recently due to high [Ethereum](https://ethereum.org/en/) profitability. You
|
||||||
[#STOPEIP1559](https://stopeip1559.org/) move. `flexpool-activity` is able to listen and notify when a new **block** is
|
can opt for the solo-mining path or use a **mining pool** to increase your chances to receive block rewards.
|
||||||
mined by the pool, display the up-to-date **miner balance** and convert it to **fiat**.
|
|
||||||
|
`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
|
## 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
|
sudo apt install python3-virtualenv
|
||||||
virtualenv venv
|
virtualenv venv
|
||||||
source venv/bin/activate
|
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
|
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
|
||||||
|
|
||||||
Configuration file use the JSON format with the following keys:
|
Configuration file use the JSON format with the following keys:
|
||||||
* `miner`: wallet address of the miner (optional)
|
* `pools`: list of mining pools
|
||||||
* `currency`: symbol of the currency to convert (optional)
|
* `miner`: wallet address of the miner
|
||||||
* `telegram`: send notifications with Telegram (optional)
|
* `currency`: symbol of the currency to convert
|
||||||
|
* `telegram`: send notifications with Telegram
|
||||||
* `auth_key`: Telegram authentication key for the bot API
|
* `auth_key`: Telegram authentication key for the bot API
|
||||||
* `chat_id`: Telegram chat room id (where to send the message)
|
* `chat_id`: Telegram chat room id (where to send the message)
|
||||||
* `state_file`: persist data between runs into this file (default: `state.json`)
|
* `state_file`: persist data between runs into this file (default: `state.json`)
|
||||||
|
|
||||||
See [configuration example](config.example.json).
|
See [configuration example](config.example.json).
|
||||||
|
|
||||||
|
All options are optional (but the companion would do nothing).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"miner": "string",
|
"pools": [
|
||||||
|
"ethermine",
|
||||||
|
"flexpool"
|
||||||
|
],
|
||||||
|
"miner": "your.eth.address",
|
||||||
"currency": "USD",
|
"currency": "USD",
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"chat_id": 123,
|
"chat_id": 123,
|
||||||
|
|
|
@ -22,6 +22,17 @@
|
||||||
"miner": {
|
"miner": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"pools": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"flexpool",
|
||||||
|
"ethermine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
"state_file": {
|
"state_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
113
flexpool.py
113
flexpool.py
|
@ -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
130
main.py
|
@ -2,12 +2,10 @@
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
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 flexpool import BlockNotFoundException, LastBlock, Miner
|
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from state import create_state, read_state, write_state
|
from state import State
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -34,81 +32,6 @@ 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 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():
|
def main():
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
setup_logging(args)
|
setup_logging(args)
|
||||||
|
@ -116,13 +39,16 @@ def main():
|
||||||
config = read_config(args.config)
|
config = read_config(args.config)
|
||||||
validate_config(config)
|
validate_config(config)
|
||||||
|
|
||||||
state_file = config.get('state_file', DEFAULT_STATE_FILE)
|
state = State(filename=config.get('state_file', DEFAULT_STATE_FILE))
|
||||||
create_state(state_file)
|
|
||||||
state = read_state(state_file)
|
|
||||||
|
|
||||||
exchange_rate = None
|
exchange_rate = None
|
||||||
currency = config.get('currency')
|
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:
|
if currency:
|
||||||
logger.debug('fetching current rate')
|
logger.debug('fetching current rate')
|
||||||
try:
|
try:
|
||||||
|
@ -131,22 +57,34 @@ def main():
|
||||||
logger.warning(f'failed to get ETH/{currency} rate')
|
logger.warning(f'failed to get ETH/{currency} rate')
|
||||||
logger.debug(str(err))
|
logger.debug(str(err))
|
||||||
|
|
||||||
block = watch_block(last_block=state.get('block'), config=config, disable_notifications=args.disable_notifications,
|
for pool in config.get('pools', []):
|
||||||
exchange_rate=exchange_rate, currency=currency)
|
pool_state = state.get(pool)
|
||||||
if block:
|
|
||||||
logger.debug('saving block number to state file')
|
|
||||||
write_state(state_file, block_number=block.number)
|
|
||||||
|
|
||||||
if config.get('miner'):
|
if pool == 'flexpool':
|
||||||
miner = watch_miner(last_balance=state.get('balance'), last_transaction=state.get('payment'),
|
from pools.flexpool import FlexpoolHandler
|
||||||
address=config['miner'], config=config, disable_notifications=args.disable_notifications,
|
handler = FlexpoolHandler(exchange_rate=exchange_rate, currency=currency, notifier=notifier)
|
||||||
exchange_rate=exchange_rate, currency=currency)
|
elif pool == 'ethermine':
|
||||||
if miner:
|
from pools.ethermine import EthermineHandler
|
||||||
logger.debug('saving miner balance to state file')
|
handler = EthermineHandler(exchange_rate=exchange_rate, currency=currency, notifier=notifier)
|
||||||
write_state(state_file, miner_balance=miner.raw_balance)
|
else:
|
||||||
if miner.last_transaction and miner.last_transaction.txid:
|
logger.warning(f'pool {pool} not supported')
|
||||||
logger.debug('saving miner payment to state file')
|
continue
|
||||||
write_state(state_file, miner_payment=miner.last_transaction.txid)
|
|
||||||
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
51
pools/__init__.py
Normal file
51
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
pools/ethermine.py
Normal file
119
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
pools/flexpool.py
Normal file
178
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
|
|
@ -1,31 +1,3 @@
|
||||||
appdirs==1.4.3
|
-r requirements/base.txt
|
||||||
attrs==20.3.0
|
-r requirements/ethermine.txt
|
||||||
CacheControl==0.12.6
|
-r requirements/flexpool.txt
|
||||||
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
|
|
||||||
|
|
29
requirements/base.txt
Normal file
29
requirements/base.txt
Normal 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
|
9
requirements/ethermine.txt
Normal file
9
requirements/ethermine.txt
Normal 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
18
requirements/flexpool.txt
Normal 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
|
48
state.py
48
state.py
|
@ -2,29 +2,33 @@ import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def read_state(filename):
|
class State:
|
||||||
with open(filename, 'r') as fd:
|
def __init__(self, filename):
|
||||||
return json.load(fd)
|
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):
|
def read(self):
|
||||||
data = {}
|
with open(self.filename, 'r') as fd:
|
||||||
if os.path.isfile(filename):
|
return json.load(fd)
|
||||||
data = read_state(filename)
|
|
||||||
|
|
||||||
if block_number:
|
def write(self, pool_name, block_number=None, miner_balance=None, miner_payment=None):
|
||||||
data['block'] = block_number
|
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:
|
def get(self, key):
|
||||||
data['balance'] = miner_balance
|
content = self.read()
|
||||||
|
return content.get(key, {})
|
||||||
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)
|
|
||||||
|
|
79
telegram.py
79
telegram.py
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
@ -8,40 +9,58 @@ logger = logging.getLogger(__name__)
|
||||||
absolute_path = os.path.split(os.path.abspath(__file__))[0]
|
absolute_path = os.path.split(os.path.abspath(__file__))[0]
|
||||||
|
|
||||||
|
|
||||||
def markdown_escape(text):
|
class TelegramNotifier:
|
||||||
text = str(text)
|
def __init__(self, chat_id, auth_key):
|
||||||
for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']:
|
self._auth_key = auth_key
|
||||||
text = text.replace(special_char, fr'\{special_char}')
|
self._default_payload = {'auth_key': auth_key, 'chat_id': chat_id, 'parse_mode': 'MarkdownV2',
|
||||||
return text
|
'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):
|
def _generate_payload(self, message_variables, template_name):
|
||||||
return generate_payload(chat_id, message_variables, 'block.md.j2')
|
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):
|
def notify_balance(self, pool, address, url, balance, balance_percentage, balance_fiat=None):
|
||||||
return generate_payload(chat_id, message_variables, 'balance.md.j2')
|
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):
|
def _send_message(self, payload):
|
||||||
return generate_payload(chat_id, message_variables, 'payment.md.j2')
|
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 generate_payload(chat_id, message_variables, template_name):
|
def _sanitize(payload):
|
||||||
payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2', 'disable_web_page_preview': True}
|
payload = copy(payload)
|
||||||
template_path = os.path.join(absolute_path, 'templates')
|
if 'auth_key' in payload:
|
||||||
loader = FileSystemLoader(template_path)
|
payload['auth_key'] = '***'
|
||||||
env = Environment(loader=loader)
|
return payload
|
||||||
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()
|
|
||||||
|
|
|
@ -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 balance*: {{balance}} {% if balance_fiat != 'None' %}\({{balance_fiat}}\){% endif %}
|
||||||
*Unpaid percentage*: {{balance_percentage}}
|
*Unpaid percentage*: {{balance_percentage}}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
*⛏️ New block*
|
*⛏️ New {{pool}} block*
|
||||||
|
|
||||||
*Number*: [{{number}}](https://etherscan.io/block/{{hash}})
|
*Number*: [{{number}}](https://etherscan.io/block/{{hash}})
|
||||||
*Reward*: {{reward}} {% if reward_fiat != 'None' %}\({{reward_fiat}}\){% endif %}
|
*Reward*: {{reward}} {% if reward_fiat != 'None' %}\({{reward_fiat}}\){% endif %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
*💵 New payment*
|
*💵 New {{pool}} payment*
|
||||||
|
|
||||||
*Amount*: {{amount}} {% if amount_fiat != 'None' %}\({{amount_fiat}}\){% endif %}
|
*Amount*: {{amount}} {% if amount_fiat != 'None' %}\({{amount_fiat}}\){% endif %}
|
||||||
*ID*: [{{txid}}](https://etherscan.io/tx/{{txid}})
|
*ID*: [{{txid}}](https://etherscan.io/tx/{{txid}})
|
||||||
|
|
15
utils.py
Normal file
15
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