Compare commits
10 commits
38d1dfb2d4
...
e6c8411615
Author | SHA1 | Date | |
---|---|---|---|
e6c8411615 | |||
46395768f7 | |||
2f0f4a9fde | |||
d01fefbfbf | |||
6445f3f292 | |||
b923db5b3c | |||
e511eb89ef | |||
d2d1503779 | |||
b519770922 | |||
e3ae989511 |
10 changed files with 398 additions and 77 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
etc
|
||||||
bin
|
bin
|
||||||
flexassistant.yaml
|
flexassistant.yaml
|
||||||
flexassistant.db
|
flexassistant.db
|
79
README.md
79
README.md
|
@ -1,3 +1,10 @@
|
||||||
|
# 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
|
# flexassistant
|
||||||
|
|
||||||
[Flexpool.io](https://www.flexpool.io/) is a famous cryptocurrency mining or farming pool supporting
|
[Flexpool.io](https://www.flexpool.io/) is a famous cryptocurrency mining or farming pool supporting
|
||||||
|
@ -54,13 +61,57 @@ ls -l bin/flexassistant
|
||||||
|
|
||||||
## Configuration
|
## 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
|
*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.
|
another file provided by the `-config` argument.
|
||||||
|
|
||||||
As a good start, you can copy the configuration file example:
|
As a good start, you can copy the configuration file example:
|
||||||
|
|
||||||
```
|
```
|
||||||
cp -p flexassistant.yaml.example flexassistant.yaml
|
cp -p flexassistant.example.yaml flexassistant.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Then edit this file at will.
|
Then edit this file at will.
|
||||||
|
@ -71,10 +122,13 @@ Reference:
|
||||||
* `max-blocks` (optional): maximum number of blocks to retreive from the API
|
* `max-blocks` (optional): maximum number of blocks to retreive from the API
|
||||||
* `max-payments` (optional): maximum number of payments to retreive from the API
|
* `max-payments` (optional): maximum number of payments to retreive from the API
|
||||||
* `pools` (optional): list of pools
|
* `pools` (optional): list of pools
|
||||||
* `coin`: coin of the pool (ex: `eth`, `xch`)
|
* `coin`: coin of the pool (ex: `etc`, `eth`, `xch`)
|
||||||
* `enable-blocks` (optional): enable block notifications for this pool (disabled by default)
|
* `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
|
* `miners` (optional): list of miners and/or farmers
|
||||||
* `address`: address of the miner or the farmer registered on the API
|
* `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-balance` (optional): enable balance notifications (disabled by default)
|
||||||
* `enable-payments` (optional): enable payments 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
|
* `enable-offline-workers` (optional): enable offline/online notifications for associated workers (disabled by
|
||||||
|
@ -83,12 +137,19 @@ Reference:
|
||||||
* `token`: token of the Telegram bot
|
* `token`: token of the Telegram bot
|
||||||
* `chat-id` (optional if `channel-name` is present): chat identifier to send Telegram notifications
|
* `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
|
* `channel-name` (optional if `chat-id` is present): channel name to send Telegram notifications
|
||||||
* `notification-templates` (optional): path to [template](https://pkg.go.dev/text/template) files for each notification
|
* `notifications` (optional): Notifications configurations
|
||||||
type
|
* `balance` (optional): balance notifications settings
|
||||||
* `balance` (optional): path to template file to format balance notifications
|
* `template` (optional): path to [template](https://pkg.go.dev/text/template) file
|
||||||
* `payment` (optional): path to template file to format payment notifications
|
* `test` (optional): send a test notification
|
||||||
* `block` (optional): path to template file to format block notifications
|
* `payment` (optional): payment notifications settings
|
||||||
* `offline-worker` (optional): path to template file to format offline worker notifications
|
* `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
|
||||||
|
|
||||||
## Templating
|
## Templating
|
||||||
|
|
||||||
|
@ -111,7 +172,7 @@ The following **data** is available to templates:
|
||||||
|
|
||||||
Default templates are available in the [templates](templates) directory.
|
Default templates are available in the [templates](templates) directory.
|
||||||
|
|
||||||
Custom template files can be used with the `notification-templates` settings (see _Configuration_ section).
|
Custom template files can be used with the `template` settings (see _Configuration_ section).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
1.3
|
1.7
|
||||||
|
|
153
client.go
153
client.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
@ -86,7 +87,8 @@ func (f *FlexpoolClient) MinerBalance(coin string, address string) (float64, err
|
||||||
type PaymentsResponse struct {
|
type PaymentsResponse struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Result struct {
|
Result struct {
|
||||||
Data []struct {
|
TotalPages int `json:"totalPages"`
|
||||||
|
Data []struct {
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
@ -97,7 +99,9 @@ type PaymentsResponse struct {
|
||||||
// MinerPayments returns an ordered list of payments
|
// MinerPayments returns an ordered list of payments
|
||||||
func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (payments []*Payment, err error) {
|
func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (payments []*Payment, err error) {
|
||||||
page := 0
|
page := 0
|
||||||
for {
|
totalPages := 0
|
||||||
|
|
||||||
|
for page <= MaxIterations && len(payments) < limit {
|
||||||
body, err := f.request(fmt.Sprintf("%s/miner/payments/?coin=%s&address=%s&page=%d", FlexpoolAPIURL, coin, address, page))
|
body, err := f.request(fmt.Sprintf("%s/miner/payments/?coin=%s&address=%s&page=%d", FlexpoolAPIURL, coin, address, page))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -106,6 +110,10 @@ func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (
|
||||||
var response PaymentsResponse
|
var response PaymentsResponse
|
||||||
json.Unmarshal(body, &response)
|
json.Unmarshal(body, &response)
|
||||||
|
|
||||||
|
if totalPages == 0 {
|
||||||
|
totalPages = response.Result.TotalPages
|
||||||
|
}
|
||||||
|
|
||||||
for _, result := range response.Result.Data {
|
for _, result := range response.Result.Data {
|
||||||
payment := NewPayment(
|
payment := NewPayment(
|
||||||
result.Hash,
|
result.Hash,
|
||||||
|
@ -114,18 +122,34 @@ func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (
|
||||||
)
|
)
|
||||||
payments = append(payments, payment)
|
payments = append(payments, payment)
|
||||||
if len(payments) >= limit {
|
if len(payments) >= limit {
|
||||||
// sort by timestamp
|
break
|
||||||
sort.Slice(payments, func(p1, p2 int) bool {
|
|
||||||
return payments[p1].Timestamp > payments[p2].Timestamp
|
|
||||||
})
|
|
||||||
return payments, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
page = page + 1
|
page++
|
||||||
if page > MaxIterations {
|
if page >= totalPages {
|
||||||
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if page > MaxIterations {
|
||||||
|
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkersResponse represents the JSON structure of the Flexpool API response for workers
|
// WorkersResponse represents the JSON structure of the Flexpool API response for workers
|
||||||
|
@ -164,7 +188,8 @@ func (f *FlexpoolClient) MinerWorkers(coin string, address string) (workers []*W
|
||||||
type BlocksResponse struct {
|
type BlocksResponse struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Result struct {
|
Result struct {
|
||||||
Data []struct {
|
TotalPages int `json:"totalPages"`
|
||||||
|
Data []struct {
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Number uint64 `json:"number"`
|
Number uint64 `json:"number"`
|
||||||
Reward float64 `json:"reward"`
|
Reward float64 `json:"reward"`
|
||||||
|
@ -175,7 +200,9 @@ type BlocksResponse struct {
|
||||||
// PoolBlocks returns an ordered list of blocks
|
// PoolBlocks returns an ordered list of blocks
|
||||||
func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, err error) {
|
func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, err error) {
|
||||||
page := 0
|
page := 0
|
||||||
for {
|
totalPages := 0
|
||||||
|
|
||||||
|
for page <= MaxIterations && len(blocks) < limit {
|
||||||
body, err := f.request(fmt.Sprintf("%s/pool/blocks/?coin=%s&page=%d", FlexpoolAPIURL, coin, page))
|
body, err := f.request(fmt.Sprintf("%s/pool/blocks/?coin=%s&page=%d", FlexpoolAPIURL, coin, page))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -184,6 +211,10 @@ func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, er
|
||||||
var response BlocksResponse
|
var response BlocksResponse
|
||||||
json.Unmarshal(body, &response)
|
json.Unmarshal(body, &response)
|
||||||
|
|
||||||
|
if totalPages == 0 {
|
||||||
|
totalPages = response.Result.TotalPages
|
||||||
|
}
|
||||||
|
|
||||||
for _, result := range response.Result.Data {
|
for _, result := range response.Result.Data {
|
||||||
block := NewBlock(
|
block := NewBlock(
|
||||||
result.Hash,
|
result.Hash,
|
||||||
|
@ -192,16 +223,98 @@ func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, er
|
||||||
)
|
)
|
||||||
blocks = append(blocks, block)
|
blocks = append(blocks, block)
|
||||||
if len(blocks) >= limit {
|
if len(blocks) >= limit {
|
||||||
// sort by number
|
break
|
||||||
sort.Slice(blocks, func(b1, b2 int) bool {
|
|
||||||
return blocks[b1].Number < blocks[b2].Number
|
|
||||||
})
|
|
||||||
return blocks, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
page = page + 1
|
page++
|
||||||
if page > MaxIterations {
|
if page >= totalPages {
|
||||||
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if page > MaxIterations {
|
||||||
|
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
38
config.go
38
config.go
|
@ -8,24 +8,26 @@ import (
|
||||||
|
|
||||||
// Config to receive settings from the configuration file
|
// Config to receive settings from the configuration file
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DatabaseFile string `yaml:"database-file"`
|
DatabaseFile string `yaml:"database-file"`
|
||||||
MaxBlocks int `yaml:"max-blocks"`
|
MaxBlocks int `yaml:"max-blocks"`
|
||||||
MaxPayments int `yaml:"max-payments"`
|
MaxPayments int `yaml:"max-payments"`
|
||||||
Pools []PoolConfig `yaml:"pools"`
|
Pools []PoolConfig `yaml:"pools"`
|
||||||
Miners []MinerConfig `yaml:"miners"`
|
Miners []MinerConfig `yaml:"miners"`
|
||||||
TelegramConfig TelegramConfig `yaml:"telegram"`
|
TelegramConfig TelegramConfig `yaml:"telegram"`
|
||||||
NotificationTemplates NotificationTemplatesConfig `yaml:"notification-templates"`
|
Notifications NotificationsConfig `yaml:"notifications"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PoolConfig to store Pool configuration
|
// PoolConfig to store Pool configuration
|
||||||
type PoolConfig struct {
|
type PoolConfig struct {
|
||||||
Coin string `yaml:"coin"`
|
Coin string `yaml:"coin"`
|
||||||
EnableBlocks bool `yaml:"enable-blocks"`
|
EnableBlocks bool `yaml:"enable-blocks"`
|
||||||
|
MinBlockReward float64 `yaml:"min-block-reward"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MinerConfig to store Miner configuration
|
// MinerConfig to store Miner configuration
|
||||||
type MinerConfig struct {
|
type MinerConfig struct {
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
|
Coin string `yaml:"coin"`
|
||||||
EnableBalance bool `yaml:"enable-balance"`
|
EnableBalance bool `yaml:"enable-balance"`
|
||||||
EnablePayments bool `yaml:"enable-payments"`
|
EnablePayments bool `yaml:"enable-payments"`
|
||||||
EnableOfflineWorkers bool `yaml:"enable-offline-workers"`
|
EnableOfflineWorkers bool `yaml:"enable-offline-workers"`
|
||||||
|
@ -38,12 +40,18 @@ type TelegramConfig struct {
|
||||||
ChannelName string `yaml:"channel-name"`
|
ChannelName string `yaml:"channel-name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotificationTemplatesConfig to store notifications templates configuration
|
// NotificationTemplatesConfig to store all notifications configurations
|
||||||
type NotificationTemplatesConfig struct {
|
type NotificationsConfig struct {
|
||||||
Balance string `yaml:"balance"`
|
Balance NotificationConfig `yaml:"balance"`
|
||||||
Payment string `yaml:"payment"`
|
Payment NotificationConfig `yaml:"payment"`
|
||||||
Block string `yaml:"block"`
|
Block NotificationConfig `yaml:"block"`
|
||||||
OfflineWorker string `yaml:"offline-worker"`
|
OfflineWorker NotificationConfig `yaml:"offline-worker"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationConfig to store a single notification configuration
|
||||||
|
type NotificationConfig struct {
|
||||||
|
Template string `yaml:"template"`
|
||||||
|
Test bool `yaml:"test"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a Config with default values
|
// NewConfig creates a Config with default values
|
||||||
|
|
|
@ -4,24 +4,36 @@ max-blocks: 10
|
||||||
max-payments: 5
|
max-payments: 5
|
||||||
miners:
|
miners:
|
||||||
- address: 0x0000000000000000000000000000000000000000
|
- address: 0x0000000000000000000000000000000000000000
|
||||||
|
coin: eth
|
||||||
enable-balance: true
|
enable-balance: true
|
||||||
enable-payments: true
|
enable-payments: true
|
||||||
enable-offline-workers: true
|
enable-offline-workers: true
|
||||||
- address: xch00000000000000000000000000000000000000000000000000000000000
|
- address: xch00000000000000000000000000000000000000000000000000000000000
|
||||||
|
coin: xch
|
||||||
enable-balance: true
|
enable-balance: true
|
||||||
enable-payments: true
|
enable-payments: true
|
||||||
enable-offline-workers: true
|
enable-offline-workers: true
|
||||||
pools:
|
pools:
|
||||||
- coin: eth
|
- coin: eth
|
||||||
enable-blocks: true
|
enable-blocks: true
|
||||||
|
min-block-reward: 10
|
||||||
- coin: xch
|
- coin: xch
|
||||||
enable-blocks: true
|
enable-blocks: true
|
||||||
|
min-block-reward: 1.79
|
||||||
telegram:
|
telegram:
|
||||||
chat-id: 000000000
|
chat-id: 000000000
|
||||||
channel-name: MyTelegramChannel
|
channel-name: '@MyTelegramChannel'
|
||||||
token: 0000000000000000000000000000000000000000000000
|
token: 0000000000000000000000000000000000000000000000
|
||||||
notification-templates:
|
#notifications:
|
||||||
balance: balance.tmpl
|
# balance:
|
||||||
block: block.tmpl
|
# template: balance.tmpl
|
||||||
offline-worker; offline-worker.tmpl
|
# test: true
|
||||||
payment: payment.tmpl
|
# block:
|
||||||
|
# template: block.tmpl
|
||||||
|
# test: true
|
||||||
|
# offline-worker:
|
||||||
|
# template: offline-worker.tmpl
|
||||||
|
# test: true
|
||||||
|
# payment:
|
||||||
|
# template: payment.tmpl
|
||||||
|
# test: true
|
22
main.go
22
main.go
|
@ -3,7 +3,9 @@ package main
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
@ -30,6 +32,7 @@ const MaxBlocks = 50
|
||||||
// initialize logging
|
// initialize logging
|
||||||
func init() {
|
func init() {
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -84,11 +87,20 @@ func main() {
|
||||||
client := NewFlexpoolClient()
|
client := NewFlexpoolClient()
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.NotificationTemplates)
|
notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.Notifications)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Could not create notifier: %v", err)
|
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
|
// Limits
|
||||||
var maxPayments int
|
var maxPayments int
|
||||||
if config.MaxPayments > 0 {
|
if config.MaxPayments > 0 {
|
||||||
|
@ -106,7 +118,7 @@ func main() {
|
||||||
|
|
||||||
// Handle miners
|
// Handle miners
|
||||||
for _, configuredMiner := range config.Miners {
|
for _, configuredMiner := range config.Miners {
|
||||||
miner, err := NewMiner(configuredMiner.Address)
|
miner, err := NewMiner(configuredMiner.Address, configuredMiner.Coin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Could not parse miner: %v", err)
|
log.Warnf("Could not parse miner: %v", err)
|
||||||
continue
|
continue
|
||||||
|
@ -263,7 +275,11 @@ func main() {
|
||||||
log.Warnf("Cannot update pool: %v", trx.Error)
|
log.Warnf("Cannot update pool: %v", trx.Error)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if notify {
|
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 {
|
||||||
err = notifier.NotifyBlock(*pool, *block)
|
err = notifier.NotifyBlock(*pool, *block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Cannot send notification: %v", err)
|
log.Warnf("Cannot send notification: %v", err)
|
||||||
|
|
14
miner.go
14
miner.go
|
@ -24,13 +24,15 @@ type Miner struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMiner creates a Miner
|
// NewMiner creates a Miner
|
||||||
func NewMiner(address string) (*Miner, error) {
|
func NewMiner(address string, coin string) (*Miner, error) {
|
||||||
miner := &Miner{Address: address}
|
miner := &Miner{Address: address, Coin: coin}
|
||||||
coin, err := miner.ParseCoin()
|
if coin == "" {
|
||||||
if err != nil {
|
coin, err := miner.ParseCoin()
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
miner.Coin = coin
|
||||||
}
|
}
|
||||||
miner.Coin = coin
|
|
||||||
return miner, nil
|
return miner, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
136
notification.go
136
notification.go
|
@ -32,19 +32,20 @@ type Notifier interface {
|
||||||
NotifyPayment(miner Miner, payment Payment) error
|
NotifyPayment(miner Miner, payment Payment) error
|
||||||
NotifyBlock(pool Pool, block Block) error
|
NotifyBlock(pool Pool, block Block) error
|
||||||
NotifyOfflineWorker(worker Worker) error
|
NotifyOfflineWorker(worker Worker) error
|
||||||
|
NotifyTest(client FlexpoolClient) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TelegramNotifier to send notifications using Telegram
|
// TelegramNotifier to send notifications using Telegram
|
||||||
// Implements the Notifier interface
|
// Implements the Notifier interface
|
||||||
type TelegramNotifier struct {
|
type TelegramNotifier struct {
|
||||||
bot *telegram.BotAPI
|
bot *telegram.BotAPI
|
||||||
chatID int64
|
chatID int64
|
||||||
channelName string
|
channelName string
|
||||||
templatesConfig *NotificationTemplatesConfig
|
configurations *NotificationsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTelegramNotifier to create a TelegramNotifier
|
// NewTelegramNotifier to create a TelegramNotifier
|
||||||
func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTemplatesConfig) (*TelegramNotifier, error) {
|
func NewTelegramNotifier(config *TelegramConfig, configurations *NotificationsConfig) (*TelegramNotifier, error) {
|
||||||
bot, err := telegram.NewBotAPI(config.Token)
|
bot, err := telegram.NewBotAPI(config.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -52,10 +53,10 @@ func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTe
|
||||||
log.Debugf("Connected to Telegram as %s", bot.Self.UserName)
|
log.Debugf("Connected to Telegram as %s", bot.Self.UserName)
|
||||||
|
|
||||||
return &TelegramNotifier{
|
return &TelegramNotifier{
|
||||||
bot: bot,
|
bot: bot,
|
||||||
chatID: config.ChatID,
|
chatID: config.ChatID,
|
||||||
channelName: config.ChannelName,
|
channelName: config.ChannelName,
|
||||||
templatesConfig: templatesConfig,
|
configurations: configurations,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,8 +126,8 @@ func fileExists(filename string) bool {
|
||||||
// Implements the Notifier interface
|
// Implements the Notifier interface
|
||||||
func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) {
|
func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) {
|
||||||
templateName := "templates/balance.tmpl"
|
templateName := "templates/balance.tmpl"
|
||||||
if t.templatesConfig.Balance != "" {
|
if t.configurations.Balance.Template != "" {
|
||||||
templateName = t.templatesConfig.Balance
|
templateName = t.configurations.Balance.Template
|
||||||
}
|
}
|
||||||
message, err := t.formatMessage(templateName, Attachment{Miner: miner})
|
message, err := t.formatMessage(templateName, Attachment{Miner: miner})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -135,12 +136,26 @@ func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) {
|
||||||
return t.sendMessage(message)
|
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
|
// NotifyPayment to format and send a notification when a new payment has been detected
|
||||||
// Implements the Notifier interface
|
// Implements the Notifier interface
|
||||||
func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error {
|
func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error {
|
||||||
templateName := "templates/payment.tmpl"
|
templateName := "templates/payment.tmpl"
|
||||||
if t.templatesConfig.Payment != "" {
|
if t.configurations.Payment.Template != "" {
|
||||||
templateName = t.templatesConfig.Payment
|
templateName = t.configurations.Payment.Template
|
||||||
}
|
}
|
||||||
message, err := t.formatMessage(templateName, Attachment{Miner: miner, Payment: payment})
|
message, err := t.formatMessage(templateName, Attachment{Miner: miner, Payment: payment})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -149,12 +164,30 @@ func (t *TelegramNotifier) NotifyPayment(miner Miner, payment Payment) error {
|
||||||
return t.sendMessage(message)
|
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
|
// NotifyBlock to format and send a notification when a new block has been detected
|
||||||
// Implements the Notifier interface
|
// Implements the Notifier interface
|
||||||
func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error {
|
func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error {
|
||||||
templateName := "templates/block.tmpl"
|
templateName := "templates/block.tmpl"
|
||||||
if t.templatesConfig.Block != "" {
|
if t.configurations.Block.Template != "" {
|
||||||
templateName = t.templatesConfig.Block
|
templateName = t.configurations.Block.Template
|
||||||
}
|
}
|
||||||
message, err := t.formatMessage(templateName, Attachment{Pool: pool, Block: block})
|
message, err := t.formatMessage(templateName, Attachment{Pool: pool, Block: block})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -163,11 +196,25 @@ func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error {
|
||||||
return t.sendMessage(message)
|
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
|
// NotifyOfflineWorker sends a message when a worker is online or offline
|
||||||
func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error {
|
func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error {
|
||||||
templateName := "templates/offline-worker.tmpl"
|
templateName := "templates/offline-worker.tmpl"
|
||||||
if t.templatesConfig.OfflineWorker != "" {
|
if t.configurations.OfflineWorker.Template != "" {
|
||||||
templateName = t.templatesConfig.OfflineWorker
|
templateName = t.configurations.OfflineWorker.Template
|
||||||
}
|
}
|
||||||
message, err := t.formatMessage(templateName, Attachment{Worker: worker})
|
message, err := t.formatMessage(templateName, Attachment{Worker: worker})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -175,3 +222,58 @@ func (t *TelegramNotifier) NotifyOfflineWorker(worker Worker) error {
|
||||||
}
|
}
|
||||||
return t.sendMessage(message)
|
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
|
||||||
|
}
|
||||||
|
|
6
utils.go
6
utils.go
|
@ -14,6 +14,8 @@ const MojoToXCHDivider = 1000000000000
|
||||||
// Example: for "eth", convert from Weis to ETH
|
// Example: for "eth", convert from Weis to ETH
|
||||||
func ConvertCurrency(coin string, value float64) (float64, error) {
|
func ConvertCurrency(coin string, value float64) (float64, error) {
|
||||||
switch coin {
|
switch coin {
|
||||||
|
case "etc":
|
||||||
|
return ConvertWeis(value), nil
|
||||||
case "eth":
|
case "eth":
|
||||||
return ConvertWeis(value), nil
|
return ConvertWeis(value), nil
|
||||||
case "xch":
|
case "xch":
|
||||||
|
@ -36,6 +38,8 @@ func ConvertMojo(value float64) float64 {
|
||||||
// FormatBlockURL returns the URL on the respective blockchain explorer given the coin and the block hash
|
// FormatBlockURL returns the URL on the respective blockchain explorer given the coin and the block hash
|
||||||
func FormatBlockURL(coin string, hash string) (string, error) {
|
func FormatBlockURL(coin string, hash string) (string, error) {
|
||||||
switch coin {
|
switch coin {
|
||||||
|
case "etc":
|
||||||
|
return fmt.Sprintf("https://etcblockexplorer.com/block/%s", hash), nil
|
||||||
case "eth":
|
case "eth":
|
||||||
return fmt.Sprintf("https://etherscan.io/block/%s", hash), nil
|
return fmt.Sprintf("https://etherscan.io/block/%s", hash), nil
|
||||||
case "xch":
|
case "xch":
|
||||||
|
@ -47,6 +51,8 @@ func FormatBlockURL(coin string, hash string) (string, error) {
|
||||||
// FormatTransactionURL returns the URL on the respective blockchain explorer given the coin and the transaction hash
|
// FormatTransactionURL returns the URL on the respective blockchain explorer given the coin and the transaction hash
|
||||||
func FormatTransactionURL(coin string, hash string) (string, error) {
|
func FormatTransactionURL(coin string, hash string) (string, error) {
|
||||||
switch coin {
|
switch coin {
|
||||||
|
case "etc":
|
||||||
|
return fmt.Sprintf("https://etcblockexplorer.com/address/%s", hash), nil
|
||||||
case "eth":
|
case "eth":
|
||||||
return fmt.Sprintf("https://etherscan.io/tx/%s", hash), nil
|
return fmt.Sprintf("https://etherscan.io/tx/%s", hash), nil
|
||||||
case "xch":
|
case "xch":
|
||||||
|
|
Reference in a new issue