Archived
1
0
Fork 0

Compare commits

..

No commits in common. "e6c8411615693822ccd5506f90c5747df63469d1" and "38d1dfb2d41699ee71108c421ef624e1eca5c878" have entirely different histories.

10 changed files with 77 additions and 398 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
etc
bin
flexassistant.yaml
flexassistant.db

View file

@ -1,10 +1,3 @@
# END OF LIFE NOTICE
> **Flexpool.io will officially wind down its operations on November 1, 2023**
[See the full
announcement](https://www.reddit.com/r/Flexpool/comments/16q72ul/action_required_flexpoolio_shutdown_notice_nov_1/).
# flexassistant
[Flexpool.io](https://www.flexpool.io/) is a famous cryptocurrency mining or farming pool supporting
@ -61,57 +54,13 @@ ls -l bin/flexassistant
## Configuration
### Telegram
Follow [this procedure](https://core.telegram.org/bots#3-how-do-i-create-a-bot) to create a bot `token`.
Then you have two possible destinations to send messages:
* channel using a `channel_name` (string)
* chat using a `chat_id` (integer)
For testing purpose, you should store the token in a variable for next sections:
```
read -s TOKEN
```
#### Chat
To get the chat identifier, you can send a message to your bot then read messages using the API:
```
curl -s -XGET "https://api.telegram.org/bot${TOKEN}/getUpdates" | jq -r ".result[].message.chat.id"
```
You can test to send messages to a chat with:
```
read CHAT_ID
curl -s -XGET "https://api.telegram.org/bot${TOKEN}/sendMessage?chat_id=${CHAT_ID}&text=hello" | jq
```
#### Channel
Public channel names can be used (example: `@mychannel`). For private channels, you should use a `chat_id` instead.
You can test to send messages to a channel with:
```
read CHANNEL_NAME
curl -s -XGET "https://api.telegram.org/bot${TOKEN}/sendMessage?chat_id=${CHANNEL_NAME}&text=hello" | jq
```
Don't forget to prefix the channel name with an `@`.
### flexassistant
*flexassistant* can be configured using a YaML file. By default, the `flexassistant.yaml` file is used but it can be
another file provided by the `-config` argument.
As a good start, you can copy the configuration file example:
```
cp -p flexassistant.example.yaml flexassistant.yaml
cp -p flexassistant.yaml.example flexassistant.yaml
```
Then edit this file at will.
@ -122,13 +71,10 @@ Reference:
* `max-blocks` (optional): maximum number of blocks to retreive from the API
* `max-payments` (optional): maximum number of payments to retreive from the API
* `pools` (optional): list of pools
* `coin`: coin of the pool (ex: `etc`, `eth`, `xch`)
* `coin`: coin of the pool (ex: `eth`, `xch`)
* `enable-blocks` (optional): enable block notifications for this pool (disabled by default)
* `min-block-reward` (optional): send notifications when block reward has reached this minimum threshold in crypto
currency unit (ETH, XCH, etc)
* `miners` (optional): list of miners and/or farmers
* `address`: address of the miner or the farmer registered on the API
* `coin` (optional): coin of the miner (ex: `etc`, `eth`, `xch`) (deduced by default, can be wrong for `etc` coin)
* `enable-balance` (optional): enable balance notifications (disabled by default)
* `enable-payments` (optional): enable payments notifications (disabled by default)
* `enable-offline-workers` (optional): enable offline/online notifications for associated workers (disabled by
@ -137,19 +83,12 @@ Reference:
* `token`: token of the Telegram bot
* `chat-id` (optional if `channel-name` is present): chat identifier to send Telegram notifications
* `channel-name` (optional if `chat-id` is present): channel name to send Telegram notifications
* `notifications` (optional): Notifications configurations
* `balance` (optional): balance notifications settings
* `template` (optional): path to [template](https://pkg.go.dev/text/template) file
* `test` (optional): send a test notification
* `payment` (optional): payment notifications settings
* `template` (optional): path to [template](https://pkg.go.dev/text/template) file
* `test` (optional): send a test notification
* `block` (optional): block notification settings
* `template` (optional): path to [template](https://pkg.go.dev/text/template) file
* `test` (optional): send a test notification
* `offline-worker` (optional): offline workers notification settings
* `template` (optional): path to [template](https://pkg.go.dev/text/template) file
* `test` (optional): send a test notification
* `notification-templates` (optional): path to [template](https://pkg.go.dev/text/template) files for each notification
type
* `balance` (optional): path to template file to format balance notifications
* `payment` (optional): path to template file to format payment notifications
* `block` (optional): path to template file to format block notifications
* `offline-worker` (optional): path to template file to format offline worker notifications
## Templating
@ -172,7 +111,7 @@ The following **data** is available to templates:
Default templates are available in the [templates](templates) directory.
Custom template files can be used with the `template` settings (see _Configuration_ section).
Custom template files can be used with the `notification-templates` settings (see _Configuration_ section).
## Usage

View file

@ -1 +1 @@
1.7
1.3

137
client.go
View file

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"sort"
"time"
@ -87,7 +86,6 @@ func (f *FlexpoolClient) MinerBalance(coin string, address string) (float64, err
type PaymentsResponse struct {
Error string `json:"error"`
Result struct {
TotalPages int `json:"totalPages"`
Data []struct {
Hash string `json:"hash"`
Value float64 `json:"value"`
@ -99,9 +97,7 @@ type PaymentsResponse struct {
// MinerPayments returns an ordered list of payments
func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (payments []*Payment, err error) {
page := 0
totalPages := 0
for page <= MaxIterations && len(payments) < limit {
for {
body, err := f.request(fmt.Sprintf("%s/miner/payments/?coin=%s&address=%s&page=%d", FlexpoolAPIURL, coin, address, page))
if err != nil {
return nil, err
@ -110,10 +106,6 @@ func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (
var response PaymentsResponse
json.Unmarshal(body, &response)
if totalPages == 0 {
totalPages = response.Result.TotalPages
}
for _, result := range response.Result.Data {
payment := NewPayment(
result.Hash,
@ -122,34 +114,18 @@ func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (
)
payments = append(payments, payment)
if len(payments) >= limit {
break
}
}
page++
if page >= totalPages {
break
}
}
if page > MaxIterations {
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
}
// Sort by timestamp
// sort by timestamp
sort.Slice(payments, func(p1, p2 int) bool {
return payments[p1].Timestamp > payments[p2].Timestamp
})
return payments, nil
}
// LastMinerPayment return the last payment of a miner
func (f *FlexpoolClient) LastMinerPayment(miner *Miner) (*Payment, error) {
log.Debugf("Fetching last payment of %s", miner)
payments, err := f.MinerPayments(miner.Coin, miner.Address, 1)
if err != nil {
return nil, err
}
return payments[0], nil
page = page + 1
if page > MaxIterations {
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
}
}
}
// WorkersResponse represents the JSON structure of the Flexpool API response for workers
@ -188,7 +164,6 @@ func (f *FlexpoolClient) MinerWorkers(coin string, address string) (workers []*W
type BlocksResponse struct {
Error string `json:"error"`
Result struct {
TotalPages int `json:"totalPages"`
Data []struct {
Hash string `json:"hash"`
Number uint64 `json:"number"`
@ -200,9 +175,7 @@ type BlocksResponse struct {
// PoolBlocks returns an ordered list of blocks
func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, err error) {
page := 0
totalPages := 0
for page <= MaxIterations && len(blocks) < limit {
for {
body, err := f.request(fmt.Sprintf("%s/pool/blocks/?coin=%s&page=%d", FlexpoolAPIURL, coin, page))
if err != nil {
return nil, err
@ -211,10 +184,6 @@ func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, er
var response BlocksResponse
json.Unmarshal(body, &response)
if totalPages == 0 {
totalPages = response.Result.TotalPages
}
for _, result := range response.Result.Data {
block := NewBlock(
result.Hash,
@ -223,98 +192,16 @@ func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, er
)
blocks = append(blocks, block)
if len(blocks) >= limit {
break
}
}
page++
if page >= totalPages {
break
}
}
if page > MaxIterations {
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
}
// Sort by number
// sort by number
sort.Slice(blocks, func(b1, b2 int) bool {
return blocks[b1].Number < blocks[b2].Number
})
return blocks, nil
}
// LastPoolBlock return the last discovered block for a given pool
func (f *FlexpoolClient) LastPoolBlock(pool *Pool) (*Block, error) {
blocks, err := f.PoolBlocks(pool.Coin, 1)
if err != nil {
return nil, err
}
return blocks[0], nil
page = page + 1
if page > MaxIterations {
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
}
// CoinsResponse represents the JSON structure of the Flexpool API response for pool coins
type CoinsResponse struct {
Error string `json:"error"`
Result struct {
Coins []struct {
Ticker string `json:"ticker"`
Name string `json:"name"`
} `json:"coins"`
} `json:"result"`
}
// RandomPool returns a random pool from the API
func (f *FlexpoolClient) RandomPool() (*Pool, error) {
log.Debug("Fetching a random pool")
body, err := f.request(fmt.Sprintf("%s/pool/coins", FlexpoolAPIURL))
if err != nil {
return nil, err
}
var response CoinsResponse
json.Unmarshal(body, &response)
randomIndex := rand.Intn(len(response.Result.Coins))
if err != nil {
return nil, err
}
randomCoin := response.Result.Coins[randomIndex]
return NewPool(randomCoin.Ticker), nil
}
// TopMinersResponse represents the JSON structure of the Flexpool API response for pool top miners
type TopMinersResponse struct {
Error string `json:"error"`
Result []struct {
Address string `json:"address"`
} `json:"result"`
}
// RandomMiner returns a random miner from the API
func (f *FlexpoolClient) RandomMiner(pool *Pool) (*Miner, error) {
log.Debug("Fetching a random miner")
body, err := f.request(fmt.Sprintf("%s/pool/topMiners?coin=%s", FlexpoolAPIURL, pool.Coin))
if err != nil {
return nil, err
}
var response TopMinersResponse
json.Unmarshal(body, &response)
randomResult := response.Result[rand.Intn(len(response.Result))]
randomMiner, err := NewMiner(randomResult.Address, pool.Coin)
if err != nil {
return nil, err
}
randomBalance, err := f.MinerBalance(pool.Coin, randomMiner.Address)
if err != nil {
return nil, err
}
randomMiner.Balance = randomBalance
return randomMiner, nil
}
// RandomWorker returns a random worker from the API
func (f *FlexpoolClient) RandomWorker(miner *Miner) (*Worker, error) {
log.Debug("Fetching a random worker")
workers, err := f.MinerWorkers(miner.Coin, miner.Address)
if err != nil {
return nil, err
}
return workers[rand.Intn(len(workers))], nil
}

View file

@ -14,20 +14,18 @@ type Config struct {
Pools []PoolConfig `yaml:"pools"`
Miners []MinerConfig `yaml:"miners"`
TelegramConfig TelegramConfig `yaml:"telegram"`
Notifications NotificationsConfig `yaml:"notifications"`
NotificationTemplates NotificationTemplatesConfig `yaml:"notification-templates"`
}
// PoolConfig to store Pool configuration
type PoolConfig struct {
Coin string `yaml:"coin"`
EnableBlocks bool `yaml:"enable-blocks"`
MinBlockReward float64 `yaml:"min-block-reward"`
}
// MinerConfig to store Miner configuration
type MinerConfig struct {
Address string `yaml:"address"`
Coin string `yaml:"coin"`
EnableBalance bool `yaml:"enable-balance"`
EnablePayments bool `yaml:"enable-payments"`
EnableOfflineWorkers bool `yaml:"enable-offline-workers"`
@ -40,18 +38,12 @@ type TelegramConfig struct {
ChannelName string `yaml:"channel-name"`
}
// NotificationTemplatesConfig to store all notifications configurations
type NotificationsConfig struct {
Balance NotificationConfig `yaml:"balance"`
Payment NotificationConfig `yaml:"payment"`
Block NotificationConfig `yaml:"block"`
OfflineWorker NotificationConfig `yaml:"offline-worker"`
}
// NotificationConfig to store a single notification configuration
type NotificationConfig struct {
Template string `yaml:"template"`
Test bool `yaml:"test"`
// NotificationTemplatesConfig to store notifications templates configuration
type NotificationTemplatesConfig struct {
Balance string `yaml:"balance"`
Payment string `yaml:"payment"`
Block string `yaml:"block"`
OfflineWorker string `yaml:"offline-worker"`
}
// NewConfig creates a Config with default values

View file

@ -4,36 +4,24 @@ max-blocks: 10
max-payments: 5
miners:
- address: 0x0000000000000000000000000000000000000000
coin: eth
enable-balance: true
enable-payments: true
enable-offline-workers: true
- address: xch00000000000000000000000000000000000000000000000000000000000
coin: xch
enable-balance: true
enable-payments: true
enable-offline-workers: true
pools:
- coin: eth
enable-blocks: true
min-block-reward: 10
- coin: xch
enable-blocks: true
min-block-reward: 1.79
telegram:
chat-id: 000000000
channel-name: '@MyTelegramChannel'
channel-name: MyTelegramChannel
token: 0000000000000000000000000000000000000000000000
#notifications:
# balance:
# template: balance.tmpl
# test: true
# block:
# template: block.tmpl
# test: true
# offline-worker:
# template: offline-worker.tmpl
# test: true
# payment:
# template: payment.tmpl
# test: true
notification-templates:
balance: balance.tmpl
block: block.tmpl
offline-worker; offline-worker.tmpl
payment: payment.tmpl

22
main.go
View file

@ -3,9 +3,7 @@ package main
import (
"flag"
"fmt"
"math/rand"
"os"
"time"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
@ -32,7 +30,6 @@ const MaxBlocks = 50
// initialize logging
func init() {
log.SetOutput(os.Stdout)
rand.Seed(time.Now().UnixNano())
}
func main() {
@ -87,20 +84,11 @@ func main() {
client := NewFlexpoolClient()
// Notifications
notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.Notifications)
notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.NotificationTemplates)
if err != nil {
log.Fatalf("Could not create notifier: %v", err)
}
executed, err := notifier.NotifyTest(*client)
if err != nil {
log.Fatalf("Could not send test notifications: %v", err)
}
if executed {
log.Debug("Exit after sending test notifications")
return
}
// Limits
var maxPayments int
if config.MaxPayments > 0 {
@ -118,7 +106,7 @@ func main() {
// Handle miners
for _, configuredMiner := range config.Miners {
miner, err := NewMiner(configuredMiner.Address, configuredMiner.Coin)
miner, err := NewMiner(configuredMiner.Address)
if err != nil {
log.Warnf("Could not parse miner: %v", err)
continue
@ -275,11 +263,7 @@ func main() {
log.Warnf("Cannot update pool: %v", trx.Error)
continue
}
convertedReward, err := ConvertCurrency(pool.Coin, block.Reward)
if err != nil {
log.Warnf("Reward for block %d cannot be converted: %v", block.Number, err)
}
if notify && convertedReward >= configuredPool.MinBlockReward {
if notify {
err = notifier.NotifyBlock(*pool, *block)
if err != nil {
log.Warnf("Cannot send notification: %v", err)

View file

@ -24,15 +24,13 @@ type Miner struct {
}
// NewMiner creates a Miner
func NewMiner(address string, coin string) (*Miner, error) {
miner := &Miner{Address: address, Coin: coin}
if coin == "" {
func NewMiner(address string) (*Miner, error) {
miner := &Miner{Address: address}
coin, err := miner.ParseCoin()
if err != nil {
return nil, err
}
miner.Coin = coin
}
return miner, nil
}

View file

@ -32,7 +32,6 @@ type Notifier interface {
NotifyPayment(miner Miner, payment Payment) error
NotifyBlock(pool Pool, block Block) error
NotifyOfflineWorker(worker Worker) error
NotifyTest(client FlexpoolClient) error
}
// TelegramNotifier to send notifications using Telegram
@ -41,11 +40,11 @@ type TelegramNotifier struct {
bot *telegram.BotAPI
chatID int64
channelName string
configurations *NotificationsConfig
templatesConfig *NotificationTemplatesConfig
}
// NewTelegramNotifier to create a TelegramNotifier
func NewTelegramNotifier(config *TelegramConfig, configurations *NotificationsConfig) (*TelegramNotifier, error) {
func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTemplatesConfig) (*TelegramNotifier, error) {
bot, err := telegram.NewBotAPI(config.Token)
if err != nil {
return nil, err
@ -56,7 +55,7 @@ func NewTelegramNotifier(config *TelegramConfig, configurations *NotificationsCo
bot: bot,
chatID: config.ChatID,
channelName: config.ChannelName,
configurations: configurations,
templatesConfig: templatesConfig,
}, nil
}
@ -126,8 +125,8 @@ func fileExists(filename string) bool {
// Implements the Notifier interface
func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) {
templateName := "templates/balance.tmpl"
if t.configurations.Balance.Template != "" {
templateName = t.configurations.Balance.Template
if t.templatesConfig.Balance != "" {
templateName = t.templatesConfig.Balance
}
message, err := t.formatMessage(templateName, Attachment{Miner: miner})
if err != nil {
@ -136,26 +135,12 @@ func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) {
return t.sendMessage(message)
}
// testNotifyBalance sends a fake balance notification
func (t *TelegramNotifier) testNotifyBalance(client FlexpoolClient) error {
log.Debug("Testing balance notification")
randomPool, err := client.RandomPool()
if err != nil {
return err
}
randomMiner, err := client.RandomMiner(randomPool)
if err != nil {
return err
}
return t.NotifyBalance(*randomMiner)
}
// NotifyPayment to format and send a notification when a new payment has been detected
// Implements the Notifier interface
func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error {
templateName := "templates/payment.tmpl"
if t.configurations.Payment.Template != "" {
templateName = t.configurations.Payment.Template
if t.templatesConfig.Payment != "" {
templateName = t.templatesConfig.Payment
}
message, err := t.formatMessage(templateName, Attachment{Miner: miner, Payment: payment})
if err != nil {
@ -164,30 +149,12 @@ func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error {
return t.sendMessage(message)
}
// testNotifyPayment sends a fake payment notification
func (t *TelegramNotifier) testNotifyPayment(client FlexpoolClient) error {
log.Debug("Testing payment notification")
randomPool, err := client.RandomPool()
if err != nil {
return err
}
randomMiner, err := client.RandomMiner(randomPool)
if err != nil {
return err
}
randomPayment, err := client.LastMinerPayment(randomMiner)
if err != nil {
return err
}
return t.NotifyPayment(*randomMiner, *randomPayment)
}
// NotifyBlock to format and send a notification when a new block has been detected
// Implements the Notifier interface
func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error {
templateName := "templates/block.tmpl"
if t.configurations.Block.Template != "" {
templateName = t.configurations.Block.Template
if t.templatesConfig.Block != "" {
templateName = t.templatesConfig.Block
}
message, err := t.formatMessage(templateName, Attachment{Pool: pool, Block: block})
if err != nil {
@ -196,25 +163,11 @@ func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error {
return t.sendMessage(message)
}
// testNotifyBlock sends a random block notification
func (t *TelegramNotifier) testNotifyBlock(client FlexpoolClient) error {
log.Debug("Testing block notification")
randomPool, err := client.RandomPool()
if err != nil {
return err
}
randomBlock, err := client.LastPoolBlock(randomPool)
if err != nil {
return err
}
return t.NotifyBlock(*randomPool, *randomBlock)
}
// NotifyOfflineWorker sends a message when a worker is online or offline
func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error {
templateName := "templates/offline-worker.tmpl"
if t.configurations.OfflineWorker.Template != "" {
templateName = t.configurations.OfflineWorker.Template
if t.templatesConfig.OfflineWorker != "" {
templateName = t.templatesConfig.OfflineWorker
}
message, err := t.formatMessage(templateName, Attachment{Worker: worker})
if err != nil {
@ -222,58 +175,3 @@ func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error {
}
return t.sendMessage(message)
}
// testNotifyOfflineWorker sends a fake worker offline notification
func (t *TelegramNotifier) testNotifyOfflineWorker(client FlexpoolClient) error {
log.Debug("Testing offline worker notification")
randomBlock, err := client.RandomPool()
if err != nil {
return err
}
randomMiner, err := client.RandomMiner(randomBlock)
if err != nil {
return err
}
randomWorker, err := client.RandomWorker(randomMiner)
if err != nil {
return err
}
log.Debugf("%s", randomWorker)
return t.NotifyOfflineWorker(*randomWorker)
}
// NotifyTest sends fake notifications
func (t *TelegramNotifier) NotifyTest(client FlexpoolClient) (executed bool, err error) {
if t.configurations.Balance.Test {
if err = t.testNotifyBalance(client); err != nil {
return false, err
} else {
executed = true
}
}
if t.configurations.Payment.Test {
if err = t.testNotifyPayment(client); err != nil {
return false, err
} else {
executed = true
}
}
if t.configurations.Block.Test {
if err = t.testNotifyBlock(client); err != nil {
return false, err
} else {
executed = true
}
}
if t.configurations.OfflineWorker.Test {
if err = t.testNotifyOfflineWorker(client); err != nil {
return false, err
} else {
executed = true
}
}
return executed, nil
}

View file

@ -14,8 +14,6 @@ const MojoToXCHDivider = 1000000000000
// Example: for "eth", convert from Weis to ETH
func ConvertCurrency(coin string, value float64) (float64, error) {
switch coin {
case "etc":
return ConvertWeis(value), nil
case "eth":
return ConvertWeis(value), nil
case "xch":
@ -38,8 +36,6 @@ func ConvertMojo(value float64) float64 {
// FormatBlockURL returns the URL on the respective blockchain explorer given the coin and the block hash
func FormatBlockURL(coin string, hash string) (string, error) {
switch coin {
case "etc":
return fmt.Sprintf("https://etcblockexplorer.com/block/%s", hash), nil
case "eth":
return fmt.Sprintf("https://etherscan.io/block/%s", hash), nil
case "xch":
@ -51,8 +47,6 @@ func FormatBlockURL(coin string, hash string) (string, error) {
// FormatTransactionURL returns the URL on the respective blockchain explorer given the coin and the transaction hash
func FormatTransactionURL(coin string, hash string) (string, error) {
switch coin {
case "etc":
return fmt.Sprintf("https://etcblockexplorer.com/address/%s", hash), nil
case "eth":
return fmt.Sprintf("https://etherscan.io/tx/%s", hash), nil
case "xch":