diff --git a/.gitignore b/.gitignore index 487e7a2..c696239 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +etc bin flexassistant.yaml flexassistant.db \ No newline at end of file diff --git a/README.md b/README.md index 7a8cced..02f6199 100644 --- a/README.md +++ b/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 [Flexpool.io](https://www.flexpool.io/) is a famous cryptocurrency mining or farming pool supporting @@ -54,13 +61,57 @@ 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.yaml.example flexassistant.yaml +cp -p flexassistant.example.yaml flexassistant.yaml ``` Then edit this file at will. @@ -71,10 +122,13 @@ 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: `eth`, `xch`) + * `coin`: coin of the pool (ex: `etc`, `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 @@ -83,12 +137,19 @@ 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 -* `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 +* `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 ## Templating @@ -111,7 +172,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 `notification-templates` settings (see _Configuration_ section). +Custom template files can be used with the `template` settings (see _Configuration_ section). ## Usage diff --git a/VERSION b/VERSION index 7e32cd5..d3bdbdf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3 +1.7 diff --git a/client.go b/client.go index b2f961d..b38e775 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math/rand" "net/http" "sort" "time" @@ -86,7 +87,8 @@ func (f *FlexpoolClient) MinerBalance(coin string, address string) (float64, err type PaymentsResponse struct { Error string `json:"error"` Result struct { - Data []struct { + TotalPages int `json:"totalPages"` + Data []struct { Hash string `json:"hash"` Value float64 `json:"value"` Timestamp int64 `json:"timestamp"` @@ -97,7 +99,9 @@ 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 - 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)) if err != nil { return nil, err @@ -106,6 +110,10 @@ 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, @@ -114,18 +122,34 @@ func (f *FlexpoolClient) MinerPayments(coin string, address string, limit int) ( ) 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 + break } } - page = page + 1 - if page > MaxIterations { - return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations) + page++ + if page >= totalPages { + 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 @@ -164,7 +188,8 @@ func (f *FlexpoolClient) MinerWorkers(coin string, address string) (workers []*W type BlocksResponse struct { Error string `json:"error"` Result struct { - Data []struct { + TotalPages int `json:"totalPages"` + Data []struct { Hash string `json:"hash"` Number uint64 `json:"number"` Reward float64 `json:"reward"` @@ -175,7 +200,9 @@ type BlocksResponse struct { // PoolBlocks returns an ordered list of blocks func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, err error) { 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)) if err != nil { return nil, err @@ -184,6 +211,10 @@ 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, @@ -192,16 +223,98 @@ func (f *FlexpoolClient) PoolBlocks(coin string, limit int) (blocks []*Block, er ) 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 + break } } - page = page + 1 - if page > MaxIterations { - return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations) + page++ + if page >= totalPages { + 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 } diff --git a/config.go b/config.go index 6d3e7ab..39afb32 100644 --- a/config.go +++ b/config.go @@ -8,24 +8,26 @@ import ( // 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"` - NotificationTemplates NotificationTemplatesConfig `yaml:"notification-templates"` + 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"` + Notifications NotificationsConfig `yaml:"notifications"` } // PoolConfig to store Pool configuration type PoolConfig struct { - Coin string `yaml:"coin"` - EnableBlocks bool `yaml:"enable-blocks"` + 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"` @@ -38,12 +40,18 @@ type TelegramConfig struct { ChannelName string `yaml:"channel-name"` } -// 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"` +// 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"` } // NewConfig creates a Config with default values diff --git a/flexassistant.yaml.example b/flexassistant.example.yaml similarity index 59% rename from flexassistant.yaml.example rename to flexassistant.example.yaml index a4586d4..1470e55 100644 --- a/flexassistant.yaml.example +++ b/flexassistant.example.yaml @@ -4,24 +4,36 @@ 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 -notification-templates: - balance: balance.tmpl - block: block.tmpl - offline-worker; offline-worker.tmpl - payment: payment.tmpl \ No newline at end of file +#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 diff --git a/main.go b/main.go index 6d426a5..f98ce5f 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,9 @@ package main import ( "flag" "fmt" + "math/rand" "os" + "time" log "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -30,6 +32,7 @@ const MaxBlocks = 50 // initialize logging func init() { log.SetOutput(os.Stdout) + rand.Seed(time.Now().UnixNano()) } func main() { @@ -84,11 +87,20 @@ func main() { client := NewFlexpoolClient() // Notifications - notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.NotificationTemplates) + notifier, err := NewTelegramNotifier(&config.TelegramConfig, &config.Notifications) 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 { @@ -106,7 +118,7 @@ func main() { // Handle miners for _, configuredMiner := range config.Miners { - miner, err := NewMiner(configuredMiner.Address) + miner, err := NewMiner(configuredMiner.Address, configuredMiner.Coin) if err != nil { log.Warnf("Could not parse miner: %v", err) continue @@ -263,7 +275,11 @@ func main() { log.Warnf("Cannot update pool: %v", trx.Error) 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) if err != nil { log.Warnf("Cannot send notification: %v", err) diff --git a/miner.go b/miner.go index 65466d6..cfbb08b 100644 --- a/miner.go +++ b/miner.go @@ -24,13 +24,15 @@ type Miner struct { } // NewMiner creates a Miner -func NewMiner(address string) (*Miner, error) { - miner := &Miner{Address: address} - coin, err := miner.ParseCoin() - if err != nil { - return nil, err +func NewMiner(address string, coin string) (*Miner, error) { + miner := &Miner{Address: address, Coin: coin} + if coin == "" { + coin, err := miner.ParseCoin() + if err != nil { + return nil, err + } + miner.Coin = coin } - miner.Coin = coin return miner, nil } diff --git a/notification.go b/notification.go index 6cf6a17..ffca65d 100644 --- a/notification.go +++ b/notification.go @@ -32,19 +32,20 @@ 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 // Implements the Notifier interface type TelegramNotifier struct { - bot *telegram.BotAPI - chatID int64 - channelName string - templatesConfig *NotificationTemplatesConfig + bot *telegram.BotAPI + chatID int64 + channelName string + configurations *NotificationsConfig } // 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) if err != nil { return nil, err @@ -52,10 +53,10 @@ func NewTelegramNotifier(config *TelegramConfig, templatesConfig *NotificationTe log.Debugf("Connected to Telegram as %s", bot.Self.UserName) return &TelegramNotifier{ - bot: bot, - chatID: config.ChatID, - channelName: config.ChannelName, - templatesConfig: templatesConfig, + bot: bot, + chatID: config.ChatID, + channelName: config.ChannelName, + configurations: configurations, }, nil } @@ -125,8 +126,8 @@ func fileExists(filename string) bool { // Implements the Notifier interface func (t *TelegramNotifier) NotifyBalance(miner Miner) (err error) { templateName := "templates/balance.tmpl" - if t.templatesConfig.Balance != "" { - templateName = t.templatesConfig.Balance + if t.configurations.Balance.Template != "" { + templateName = t.configurations.Balance.Template } message, err := t.formatMessage(templateName, Attachment{Miner: miner}) if err != nil { @@ -135,12 +136,26 @@ 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.templatesConfig.Payment != "" { - templateName = t.templatesConfig.Payment + if t.configurations.Payment.Template != "" { + templateName = t.configurations.Payment.Template } message, err := t.formatMessage(templateName, Attachment{Miner: miner, Payment: payment}) if err != nil { @@ -149,12 +164,30 @@ 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.templatesConfig.Block != "" { - templateName = t.templatesConfig.Block + if t.configurations.Block.Template != "" { + templateName = t.configurations.Block.Template } message, err := t.formatMessage(templateName, Attachment{Pool: pool, Block: block}) if err != nil { @@ -163,11 +196,25 @@ 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.templatesConfig.OfflineWorker != "" { - templateName = t.templatesConfig.OfflineWorker + if t.configurations.OfflineWorker.Template != "" { + templateName = t.configurations.OfflineWorker.Template } message, err := t.formatMessage(templateName, Attachment{Worker: worker}) if err != nil { @@ -175,3 +222,58 @@ 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 +} diff --git a/utils.go b/utils.go index 88c29dd..97895e2 100644 --- a/utils.go +++ b/utils.go @@ -14,6 +14,8 @@ 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": @@ -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 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": @@ -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 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":