feat: Initial release (1.0)
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
c318df8bf3
commit
f51679193a
16 changed files with 956 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
bin
|
||||||
|
flexassistant.yaml
|
||||||
|
flexassistant.db
|
17
Makefile
Normal file
17
Makefile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
APPVERSION := $(shell cat ./VERSION)
|
||||||
|
GOVERSION := $(shell go version | awk '{print $$3}')
|
||||||
|
GITCOMMIT := $(shell git log -1 --oneline | awk '{print $$1}')
|
||||||
|
LDFLAGS = -X main.AppVersion=${APPVERSION} -X main.GoVersion=${GOVERSION} -X main.GitCommit=${GITCOMMIT}
|
||||||
|
PLATFORM := $(shell uname -s)
|
||||||
|
ARCH := $(shell uname -m)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -ldflags "${LDFLAGS}" -o bin/flexassistant *.go
|
||||||
|
|
||||||
|
release:
|
||||||
|
go build -ldflags "${LDFLAGS}" -o bin/flexassistant-${APPVERSION}-${PLATFORM}-${ARCH} *.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf bin
|
101
README.md
Normal file
101
README.md
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
# flexassistant
|
||||||
|
|
||||||
|
[Flexpool.io](https://www.flexpool.io/) is a famous cryptocurrency mining or farming pool supporting
|
||||||
|
[Ethereum](https://ethereum.org/en/) and [Chia](https://www.chia.net/) blockchains. As a miner, or a farmer, we like to
|
||||||
|
get **notified** when a **block** is mined, or farmed. We also like to keep track of our **unpaid balance** and our
|
||||||
|
**transactions** to our personal wallet.
|
||||||
|
|
||||||
|
*flexassistant* is a tool that parses the Flexpool API and sends notifications via [Telegram](https://telegram.org/).
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="static/screenshot.jpg" width="300" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
*Note: this guide has been written with Linux x86_64 in mind.*
|
||||||
|
|
||||||
|
### Binaries
|
||||||
|
|
||||||
|
Go to [Releases](/releases) to download the binary in the version you like (latest is recommended).
|
||||||
|
|
||||||
|
Then extract the tarball:
|
||||||
|
|
||||||
|
```
|
||||||
|
tar xvpzf flexassistant-VERSION-Linux-x86_64.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
Write checksum information to a local file:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo checksum > flexassistant-VERSION-Linux-x86_64.sha256sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify checksums to avoid binary corruption:
|
||||||
|
|
||||||
|
```
|
||||||
|
sha256sum -c flexassistant-VERSION-Linux-x86_64.sha256sum
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compilation
|
||||||
|
|
||||||
|
You will need to install [Go](https://golang.org/dl/), [Git](https://git-scm.com/) and a development toolkit (including [make](https://linux.die.net/man/1/make)) for your environment.
|
||||||
|
|
||||||
|
Then, you'll need to download and compile the source code:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/jouir/flexassistant.git
|
||||||
|
cd flexassistant
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary will be available under the `bin` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
ls -l bin/flexassistant
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
*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.yaml.example flexassistant.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Then edit this file at will.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
* `database-file` (optional): file name of the database file to persist information between two executions (SQLite database)
|
||||||
|
* `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: `eth`, `xch`)
|
||||||
|
* `enable-blocks` (optional): enable block notifications for this pool (disabled by default)
|
||||||
|
* `miners` (optional): list of miners and/or farmers
|
||||||
|
* `address`: address of the miner or the farmer registered on the API
|
||||||
|
* `enable-balance` (optional): enable balance notifications (disabled by default)
|
||||||
|
* `enable-payments` (optional): enable payments notifications (disabled by default)
|
||||||
|
* `telegram`: Telegram configuration
|
||||||
|
* `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
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage of ./flexassistant:
|
||||||
|
-config string
|
||||||
|
Configuration file name (default "flexassistant.yaml")
|
||||||
|
-debug
|
||||||
|
Print even more logs
|
||||||
|
-quiet
|
||||||
|
Log errors only
|
||||||
|
-verbose
|
||||||
|
Print more logs
|
||||||
|
-version
|
||||||
|
Print version and exit
|
||||||
|
```
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
1.0
|
135
client.go
Normal file
135
client.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FlexpoolAPIURL constant to store Flexpool API URL
|
||||||
|
const FlexpoolAPIURL = "https://api.flexpool.io/v2"
|
||||||
|
|
||||||
|
// MaxIterations to avoid infinite loop while requesting paged routes on Flexpool API
|
||||||
|
const MaxIterations = 10
|
||||||
|
|
||||||
|
// UserAgent to identify ourselves on the Flexpool API
|
||||||
|
var UserAgent = fmt.Sprintf("flexassistant/%s", AppVersion)
|
||||||
|
|
||||||
|
// FlexpoolClient to store the HTTP client
|
||||||
|
type FlexpoolClient struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFlexpoolClient to create a client to manage Flexpool API calls
|
||||||
|
func NewFlexpoolClient() *FlexpoolClient {
|
||||||
|
return &FlexpoolClient{
|
||||||
|
client: &http.Client{Timeout: time.Second * 3},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// request to create an HTTPS request, call the Flexpool API, detect errors and return the result
|
||||||
|
func (f *FlexpoolClient) request(url string) (result map[string]interface{}, err error) {
|
||||||
|
log.Debugf("Requesting %s", url)
|
||||||
|
|
||||||
|
request, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header.Set("User-Agent", UserAgent)
|
||||||
|
|
||||||
|
resp, err := f.client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
jsonBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
json.Unmarshal(jsonBody, &result)
|
||||||
|
|
||||||
|
if result["error"] == nil {
|
||||||
|
return result["result"].(map[string]interface{}), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Flexpool API error: %s", result["error"].(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinerBalance returns the current unpaid balance
|
||||||
|
func (f *FlexpoolClient) MinerBalance(coin string, address string) (float64, error) {
|
||||||
|
response, err := f.request(fmt.Sprintf("%s/miner/balance?coin=%s&address=%s", FlexpoolAPIURL, coin, address))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return response["balance"].(float64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinerPayments returns an ordered list of payments
|
||||||
|
func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) (payments []*Payment, err error) {
|
||||||
|
page := 0
|
||||||
|
for {
|
||||||
|
response, err := f.request(fmt.Sprintf("%s/miner/payments/?coin=%s&address=%s&page=%d", FlexpoolAPIURL, coin, address, page))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range response["data"].([]interface{}) {
|
||||||
|
raw := result.(map[string]interface{})
|
||||||
|
payment := NewPayment(
|
||||||
|
raw["hash"].(string),
|
||||||
|
raw["value"].(float64),
|
||||||
|
raw["timestamp"].(float64),
|
||||||
|
)
|
||||||
|
payments = append(payments, payment)
|
||||||
|
if len(payments) >= limit {
|
||||||
|
// sort by timestamp
|
||||||
|
sort.Slice(payments, func(p1, p2 int) bool {
|
||||||
|
return payments[p1].Timestamp > payments[p2].Timestamp
|
||||||
|
})
|
||||||
|
return payments, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page = page + 1
|
||||||
|
if page > MaxIterations {
|
||||||
|
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PoolBlocks returns an ordered list of blocks
|
||||||
|
func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, err error) {
|
||||||
|
page := 0
|
||||||
|
for {
|
||||||
|
response, err := f.request(fmt.Sprintf("%s/pool/blocks/?coin=%s&page=%d", FlexpoolAPIURL, coin, page))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range response["data"].([]interface{}) {
|
||||||
|
raw := result.(map[string]interface{})
|
||||||
|
block := NewBlock(
|
||||||
|
raw["hash"].(string),
|
||||||
|
raw["number"].(float64),
|
||||||
|
raw["reward"].(float64),
|
||||||
|
)
|
||||||
|
blocks = append(blocks, block)
|
||||||
|
if len(blocks) >= limit {
|
||||||
|
// sort by number
|
||||||
|
sort.Slice(blocks, func(b1, b2 int) bool {
|
||||||
|
return blocks[b1].Number < blocks[b2].Number
|
||||||
|
})
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page = page + 1
|
||||||
|
if page > MaxIterations {
|
||||||
|
return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
config.go
Normal file
59
config.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config to receive settings from the configuration file
|
||||||
|
type Config struct {
|
||||||
|
DatabaseFile string `yaml:"database-file"`
|
||||||
|
MaxBlocks int `yaml:"max-blocks"`
|
||||||
|
MaxPayments int `yaml:"max-payments"`
|
||||||
|
Pools []PoolConfig `yaml:"pools"`
|
||||||
|
Miners []MinerConfig `yaml:"miners"`
|
||||||
|
TelegramConfig TelegramConfig `yaml:"telegram"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PoolConfig to store Pool configuration
|
||||||
|
type PoolConfig struct {
|
||||||
|
Coin string `yaml:"coin"`
|
||||||
|
EnableBlocks bool `yaml:"enable-blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinerConfig to store Miner configuration
|
||||||
|
type MinerConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
EnableBalance bool `yaml:"enable-balance"`
|
||||||
|
EnablePayments bool `yaml:"enable-payments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TelegramConfig to store Telegram configuration
|
||||||
|
type TelegramConfig struct {
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
ChatID int64 `yaml:"chat-id"`
|
||||||
|
ChannelName string `yaml:"channel-name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a Config with default values
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
DatabaseFile: AppName + ".db",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile reads and parses a YAML configuration file to override default values
|
||||||
|
func (c *Config) ReadFile(filename string) (err error) {
|
||||||
|
yamlFile, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(yamlFile, &c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
22
db.go
Normal file
22
db.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDatabase creates a SQLite database object from a file name
|
||||||
|
func NewDatabase(filename string) (*gorm.DB, error) {
|
||||||
|
return gorm.Open(sqlite.Open(filename), &gorm.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDatabaseObjects creates database relations
|
||||||
|
func CreateDatabaseObjects(db *gorm.DB) error {
|
||||||
|
if err := db.AutoMigrate(&Miner{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&Pool{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
20
flexassistant.yaml.example
Normal file
20
flexassistant.yaml.example
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
database-file: flexassistant.db
|
||||||
|
max-blocks: 10
|
||||||
|
max-payments: 5
|
||||||
|
miners:
|
||||||
|
- address: 0x0000000000000000000000000000000000000000
|
||||||
|
enable-balance: true
|
||||||
|
enable-payments: true
|
||||||
|
- address: xch00000000000000000000000000000000000000000000000000000000000
|
||||||
|
enable-balance: true
|
||||||
|
enable-payments: true
|
||||||
|
pools:
|
||||||
|
- coin: eth
|
||||||
|
enable-blocks: true
|
||||||
|
- coin: xch
|
||||||
|
enable-blocks: true
|
||||||
|
telegram:
|
||||||
|
chat-id: 000000000
|
||||||
|
channel-name: MyTelegramChannel
|
||||||
|
token: 0000000000000000000000000000000000000000000000
|
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module github.com/jouir/flexassistant
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1
|
||||||
|
github.com/leekchan/accounting v1.0.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
|
gorm.io/driver/sqlite v1.1.5
|
||||||
|
gorm.io/gorm v1.21.15
|
||||||
|
)
|
37
go.sum
Normal file
37
go.sum
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1 h1:Mr8jIV7wDfLw5Fw6BPupm0aduTFdLjhI3wFuIIZKvO4=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1/go.mod h1:2s/IzRcxCszyNh760IjJiqoYHTnifk8ZeNYL33z8Pww=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||||
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/leekchan/accounting v1.0.0 h1:+Wd7dJ//dFPa28rc1hjyy+qzCbXPMR91Fb6F1VGTQHg=
|
||||||
|
github.com/leekchan/accounting v1.0.0/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||||
|
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.1.5 h1:JU8G59VyKu1x1RMQgjefQnkZjDe9wHc1kARDZPu5dZs=
|
||||||
|
gorm.io/driver/sqlite v1.1.5/go.mod h1:NpaYMcVKEh6vLJ47VP6T7Weieu4H1Drs3dGD/K6GrGc=
|
||||||
|
gorm.io/gorm v1.21.15 h1:gAyaDoPw0lCyrSFWhBlahbUA1U4P5RViC1uIqoB+1Rk=
|
||||||
|
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
239
main.go
Normal file
239
main.go
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppName to store application name
|
||||||
|
var AppName string = "flexassistant"
|
||||||
|
|
||||||
|
// AppVersion to set version at compilation time
|
||||||
|
var AppVersion string = "9999"
|
||||||
|
|
||||||
|
// GitCommit to set git commit at compilation time (can be empty)
|
||||||
|
var GitCommit string
|
||||||
|
|
||||||
|
// GoVersion to set Go version at compilation time
|
||||||
|
var GoVersion string
|
||||||
|
|
||||||
|
// MaxPayments defaults
|
||||||
|
const MaxPayments = 10
|
||||||
|
|
||||||
|
// MaxBlocks defaults
|
||||||
|
const MaxBlocks = 50
|
||||||
|
|
||||||
|
// initialize logging
|
||||||
|
func init() {
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := NewConfig()
|
||||||
|
version := flag.Bool("version", false, "Print version and exit")
|
||||||
|
quiet := flag.Bool("quiet", false, "Log errors only")
|
||||||
|
verbose := flag.Bool("verbose", false, "Print more logs")
|
||||||
|
debug := flag.Bool("debug", false, "Print even more logs")
|
||||||
|
configFileName := flag.String("config", AppName+".yaml", "Configuration file name")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *version {
|
||||||
|
showVersion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs and configuration
|
||||||
|
log.SetLevel(log.WarnLevel)
|
||||||
|
if *debug {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
}
|
||||||
|
if *verbose {
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
}
|
||||||
|
if *quiet {
|
||||||
|
log.SetLevel(log.ErrorLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *configFileName != "" {
|
||||||
|
err := config.ReadFile(*configFileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot parse configuration file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database
|
||||||
|
var db *gorm.DB
|
||||||
|
db, err := NewDatabase(config.DatabaseFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not create database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CreateDatabaseObjects(db); err != nil {
|
||||||
|
log.Fatalf("Could not create objects: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// API client
|
||||||
|
client := NewFlexpoolClient()
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
notifier, err := NewTelegramNotifier(&config.TelegramConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not create notifier: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
var maxPayments int
|
||||||
|
if config.MaxPayments > 0 {
|
||||||
|
maxPayments = config.MaxPayments
|
||||||
|
} else {
|
||||||
|
maxPayments = MaxPayments
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxBlocks int
|
||||||
|
if config.MaxBlocks > 0 {
|
||||||
|
maxBlocks = config.MaxBlocks
|
||||||
|
} else {
|
||||||
|
maxBlocks = MaxBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle miners
|
||||||
|
for _, configuredMiner := range config.Miners {
|
||||||
|
miner, err := NewMiner(configuredMiner.Address)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Could not parse miner: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbMiner Miner
|
||||||
|
trx := db.Where(Miner{Address: miner.Address}).Attrs(Miner{Address: miner.Address, Coin: miner.Coin}).FirstOrCreate(&dbMiner)
|
||||||
|
if trx.Error != nil {
|
||||||
|
log.Warnf("Cannot fetch miner %s from database: %v", miner, trx.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Balance management
|
||||||
|
if configuredMiner.EnableBalance {
|
||||||
|
// Balance have never been persisted, skip notifications
|
||||||
|
notify := true
|
||||||
|
if dbMiner.Balance == 0 {
|
||||||
|
notify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Fetching balance for %s", miner)
|
||||||
|
balance, err := client.MinerBalance(miner.Coin, miner.Address)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Could not fetch unpaid balance: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Debugf("Unpaid balance %.0f", balance)
|
||||||
|
miner.Balance = balance
|
||||||
|
if miner.Balance != dbMiner.Balance {
|
||||||
|
dbMiner.Balance = balance
|
||||||
|
if trx = db.Save(&dbMiner); trx.Error != nil {
|
||||||
|
log.Warnf("Cannot update miner: %v", trx.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if notify {
|
||||||
|
err = notifier.NotifyBalance(*miner)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Cannot send notification: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Infof("Balance notification sent for %s", miner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payments management
|
||||||
|
if configuredMiner.EnablePayments {
|
||||||
|
// Payments have never been persisted, skip notifications
|
||||||
|
notify := true
|
||||||
|
if dbMiner.LastPaymentTimestamp == 0 {
|
||||||
|
notify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Fetching payments for %s", miner)
|
||||||
|
|
||||||
|
payments, err := client.MinerPayments(miner.Coin, miner.Address, maxPayments)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Could not fetch payments: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, payment := range payments {
|
||||||
|
log.Debugf("Fetched %s", payment)
|
||||||
|
if dbMiner.LastPaymentTimestamp < payment.Timestamp {
|
||||||
|
dbMiner.LastPaymentTimestamp = payment.Timestamp
|
||||||
|
if trx = db.Save(&dbMiner); trx.Error != nil {
|
||||||
|
log.Warnf("Cannot update miner: %v", trx.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if notify {
|
||||||
|
err = notifier.NotifyPayment(*miner, *payment)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Cannot send notification: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Infof("Payment notification sent for %s", payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pools
|
||||||
|
for _, configuredPool := range config.Pools {
|
||||||
|
|
||||||
|
pool := NewPool(configuredPool.Coin)
|
||||||
|
|
||||||
|
var dbPool Pool
|
||||||
|
trx := db.Where(Pool{Coin: pool.Coin}).Attrs(Pool{Coin: pool.Coin}).FirstOrCreate(&dbPool)
|
||||||
|
if trx.Error != nil {
|
||||||
|
log.Warnf("Cannot fetch pool %s from database: %v", pool, trx.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks management
|
||||||
|
if configuredPool.EnableBlocks {
|
||||||
|
|
||||||
|
// Block number has never been persisted, skip notifications
|
||||||
|
notify := true
|
||||||
|
if dbPool.LastBlockNumber == 0 {
|
||||||
|
notify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Fetching blocks for %s", pool)
|
||||||
|
blocks, err := client.PoolBlocks(pool.Coin, maxBlocks)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Could not fetch blocks: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, block := range blocks {
|
||||||
|
log.Debugf("Fetched %s", block)
|
||||||
|
if dbPool.LastBlockNumber < block.Number {
|
||||||
|
dbPool.LastBlockNumber = block.Number
|
||||||
|
if trx = db.Save(&dbPool); trx.Error != nil {
|
||||||
|
log.Warnf("Cannot update pool: %v", trx.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if notify {
|
||||||
|
err = notifier.NotifyBlock(*pool, *block)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Cannot send notification: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Infof("Block notification sent for %s", block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showVersion() {
|
||||||
|
if GitCommit != "" {
|
||||||
|
AppVersion = fmt.Sprintf("%s-%s", AppVersion, GitCommit)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s version %s (compiled with %s)\n", AppName, AppVersion, GoVersion)
|
||||||
|
}
|
74
miner.go
Normal file
74
miner.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CoinLengthETH represents the size of an ETH address
|
||||||
|
const CoinLengthETH = 42
|
||||||
|
|
||||||
|
// CoinLengthXCH represents the size of an XCH address
|
||||||
|
const CoinLengthXCH = 62
|
||||||
|
|
||||||
|
// Miner to store miner attributes
|
||||||
|
type Miner struct {
|
||||||
|
gorm.Model
|
||||||
|
Coin string
|
||||||
|
Address string `gorm:"unique;not null"`
|
||||||
|
Balance float64
|
||||||
|
LastPaymentTimestamp float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMiner creates a Miner
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCoin deduces the currency given the miner address
|
||||||
|
func (m *Miner) ParseCoin() (coin string, err error) {
|
||||||
|
if m.Address == "" {
|
||||||
|
return "", fmt.Errorf("Miner address is empty")
|
||||||
|
}
|
||||||
|
if len(m.Address) == CoinLengthETH && strings.HasPrefix(m.Address, "0x") {
|
||||||
|
return "eth", nil
|
||||||
|
}
|
||||||
|
if len(m.Address) == CoinLengthXCH && strings.HasPrefix(m.Address, "xch") {
|
||||||
|
return "xch", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Unsupported address")
|
||||||
|
}
|
||||||
|
|
||||||
|
// String represents Miner to a printable format
|
||||||
|
func (m *Miner) String() string {
|
||||||
|
return fmt.Sprintf("Miner<%s>", m.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment to store payment attributes
|
||||||
|
type Payment struct {
|
||||||
|
Hash string
|
||||||
|
Value float64
|
||||||
|
Timestamp float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPayment creates a Payment
|
||||||
|
func NewPayment(hash string, value float64, timestamp float64) *Payment {
|
||||||
|
return &Payment{
|
||||||
|
Hash: hash,
|
||||||
|
Value: value,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String represents a Payment to a printable format
|
||||||
|
func (p *Payment) String() string {
|
||||||
|
return fmt.Sprintf("Payment<%s>", p.Hash)
|
||||||
|
}
|
123
notification.go
Normal file
123
notification.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/leekchan/accounting"
|
||||||
|
|
||||||
|
telegram "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notifier interface to define how to send all kind of notifications
|
||||||
|
type Notifier interface {
|
||||||
|
NotifyBalance(miner Miner, difference float64) error
|
||||||
|
NotifyPayment(miner Miner, payment Payment) error
|
||||||
|
NotifyBlock(pool Pool, block Block) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TelegramNotifier to send notifications using Telegram
|
||||||
|
// Implements the Notifier interface
|
||||||
|
type TelegramNotifier struct {
|
||||||
|
bot *telegram.BotAPI
|
||||||
|
chatID int64
|
||||||
|
channelName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTelegramNotifier to create a TelegramNotifier
|
||||||
|
func NewTelegramNotifier(config *TelegramConfig) (*TelegramNotifier, error) {
|
||||||
|
bot, err := telegram.NewBotAPI(config.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("Connected to Telegram as %s", bot.Self.UserName)
|
||||||
|
|
||||||
|
return &TelegramNotifier{
|
||||||
|
bot: bot,
|
||||||
|
chatID: config.ChatID,
|
||||||
|
channelName: config.ChannelName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMessage to send a generic message on Telegram
|
||||||
|
func (t *TelegramNotifier) sendMessage(message string) error {
|
||||||
|
var request telegram.MessageConfig
|
||||||
|
if t.chatID != 0 {
|
||||||
|
request = telegram.NewMessage(t.chatID, message)
|
||||||
|
} else {
|
||||||
|
request = telegram.NewMessageToChannel(t.channelName, message)
|
||||||
|
}
|
||||||
|
request.DisableWebPagePreview = true
|
||||||
|
request.ParseMode = telegram.ModeMarkdown
|
||||||
|
|
||||||
|
response, err := t.bot.Send(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("Message %d sent to Telegram", response.MessageID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyBalance to format and send a notification when the unpaid balance has changed
|
||||||
|
// Implements the Notifier interface
|
||||||
|
func (t *TelegramNotifier) NotifyBalance(miner Miner) error {
|
||||||
|
ac := accounting.Accounting{
|
||||||
|
Symbol: strings.ToUpper(miner.Coin),
|
||||||
|
Precision: 6,
|
||||||
|
Format: "%v %s",
|
||||||
|
}
|
||||||
|
convertedBalance, err := ConvertCurrency(miner.Coin, miner.Balance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("💰 *Balance* _%s_", ac.FormatMoney(convertedBalance))
|
||||||
|
return t.sendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
ac := accounting.Accounting{
|
||||||
|
Symbol: strings.ToUpper(miner.Coin),
|
||||||
|
Precision: 6,
|
||||||
|
Format: "%v %s",
|
||||||
|
}
|
||||||
|
convertedValue, err := ConvertCurrency(miner.Coin, payment.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("💵 *Payment* _%s_", ac.FormatMoney(convertedValue))
|
||||||
|
return t.sendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
precision := 6
|
||||||
|
if pool.Coin == "xch" {
|
||||||
|
precision = 2
|
||||||
|
}
|
||||||
|
ac := accounting.Accounting{
|
||||||
|
Symbol: strings.ToUpper(pool.Coin),
|
||||||
|
Precision: precision,
|
||||||
|
Format: "%v %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedValue, err := ConvertCurrency(pool.Coin, block.Reward)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
verb, err := ConvertAction(pool.Coin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
url, err := FormatBlockURL(pool.Coin, block.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
message := fmt.Sprintf("🎉 *%s* [#%.0f](%s) _%s_", verb, block.Number, url, ac.FormatMoney(convertedValue))
|
||||||
|
return t.sendMessage(message)
|
||||||
|
}
|
45
pool.go
Normal file
45
pool.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pool to store pool attributes
|
||||||
|
type Pool struct {
|
||||||
|
gorm.Model
|
||||||
|
Coin string `gorm:"unique;not null"`
|
||||||
|
LastBlockNumber float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool creates a Pool
|
||||||
|
func NewPool(coin string) *Pool {
|
||||||
|
return &Pool{Coin: coin}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String represents Pool to a printable format
|
||||||
|
func (p *Pool) String() string {
|
||||||
|
return fmt.Sprintf("Pool<%s>", p.Coin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block to store block attributes
|
||||||
|
type Block struct {
|
||||||
|
Hash string `gorm:"unique;not null"`
|
||||||
|
Number float64 `gorm:"not null"`
|
||||||
|
Reward float64 `gorm:"not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlock creates a Block
|
||||||
|
func NewBlock(hash string, number float64, reward float64) *Block {
|
||||||
|
return &Block{
|
||||||
|
Hash: hash,
|
||||||
|
Number: number,
|
||||||
|
Reward: reward,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String represents Block to a printable format
|
||||||
|
func (b *Block) String() string {
|
||||||
|
return fmt.Sprintf("Block<%.0f>", b.Number)
|
||||||
|
}
|
BIN
static/screenshot.jpg
Normal file
BIN
static/screenshot.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
68
utils.go
Normal file
68
utils.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WeisToETHDivider to divide Weis to ETH
|
||||||
|
const WeisToETHDivider = 1000000000000000000
|
||||||
|
|
||||||
|
// MojoToXCHDivider to divide Mojo to XCH
|
||||||
|
const MojoToXCHDivider = 1000000000000
|
||||||
|
|
||||||
|
// ConvertCurrency divides the smallest unit of the currency to the currency itself
|
||||||
|
// Example: for "eth", convert from Weis to ETH
|
||||||
|
func ConvertCurrency(coin string, value float64) (float64, error) {
|
||||||
|
switch coin {
|
||||||
|
case "eth":
|
||||||
|
return ConvertWeis(value), nil
|
||||||
|
case "xch":
|
||||||
|
return ConvertMojo(value), nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("Coin %s not supported", coin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertWeis converts the value from Weis to ETH
|
||||||
|
func ConvertWeis(value float64) float64 {
|
||||||
|
return value / WeisToETHDivider
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertMojo converts the value from Mojo to XCH
|
||||||
|
func ConvertMojo(value float64) float64 {
|
||||||
|
return value / MojoToXCHDivider
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertAction returns "Miner" for Ethereum and "Farmer" for Chia
|
||||||
|
// Because Chia is farmed and not mined
|
||||||
|
func ConvertAction(coin string) (string, error) {
|
||||||
|
switch coin {
|
||||||
|
case "eth":
|
||||||
|
return "Mined", nil
|
||||||
|
case "xch":
|
||||||
|
return "Farmed", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Coin %s not supported", coin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "eth":
|
||||||
|
return fmt.Sprintf("https://etherscan.io/block/%s", hash), nil
|
||||||
|
case "xch":
|
||||||
|
return fmt.Sprintf("https://www.chiaexplorer.com/blockchain/block/%s", hash), nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Coin %s not supported", coin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "eth":
|
||||||
|
return fmt.Sprintf("https://etherscan.io/tx/%s", hash), nil
|
||||||
|
case "xch":
|
||||||
|
return fmt.Sprintf("https://www.chiaexplorer.com/blockchain/coin/%s", hash), nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Coin %s not supported", coin)
|
||||||
|
}
|
Reference in a new issue