From ebbd4ac2cdd86e4f7f3141fa236891f368d7268f Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Mon, 11 Oct 2021 08:54:37 +0200 Subject: [PATCH] refactor: Use struct to parse API responses - Return bytes for each request to the Flexpool API - Delegate JSON marshalling the higher functions - Use int64 when possible BREAKING CHANGE: database structure has to be updated to use integer instead of real. Please follow instructions to upgrade from 1.0 to 1.1. Signed-off-by: Julien Riou --- UPGRADES.md | 9 ++++ client.go | 104 +++++++++++++++++++++----------------- main.go | 2 +- migrations/1.0_to_1.1.sql | 49 ++++++++++++++++++ miner.go | 10 ++-- notification.go | 2 +- pool.go | 12 ++--- utils.go | 10 ++-- 8 files changed, 133 insertions(+), 65 deletions(-) create mode 100644 UPGRADES.md create mode 100644 migrations/1.0_to_1.1.sql diff --git a/UPGRADES.md b/UPGRADES.md new file mode 100644 index 0000000..922bb6f --- /dev/null +++ b/UPGRADES.md @@ -0,0 +1,9 @@ +# 1.0 to 1.1 + +Some numeric types have been updated from **float64** to **int64**. Upgrade the database types by running the following migration: + +``` +sqlite3 flexassistant.db < migrations/1.0_to_1.1.sql +``` + +And replace `flexassistant.db` by the content of the `database_file` setting if it's has been changed. \ No newline at end of file diff --git a/client.go b/client.go index 155515f..aa34fa5 100644 --- a/client.go +++ b/client.go @@ -32,37 +32,8 @@ func NewFlexpoolClient() *FlexpoolClient { } } -// 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 nil, fmt.Errorf("Flexpool API error: %s", result["error"].(string)) - } - return result["result"].(map[string]interface{}), nil -} - -// requestBytes to create an HTTPS request, call the Flexpool API, detect errors and return the result in bytes -func (f *FlexpoolClient) requestBytes(url string) ([]byte, error) { +// request to create an HTTPS request, call the Flexpool API, detect errors and return the result in bytes +func (f *FlexpoolClient) request(url string) ([]byte, error) { log.Debugf("Requesting %s", url) request, err := http.NewRequest("GET", url, nil) @@ -91,30 +62,55 @@ func (f *FlexpoolClient) requestBytes(url string) ([]byte, error) { return jsonBody, nil } +// BalanceResponse represents the JSON structure of the Flexpool API response for balance +type BalanceResponse struct { + Error string `json:"error"` + Result struct { + Balance int64 `json:"balance"` + } `json:"result"` +} + // 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)) +func (f *FlexpoolClient) MinerBalance(coin string, address string) (int64, error) { + body, 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 + + var response BalanceResponse + json.Unmarshal(body, &response) + return response.Result.Balance, nil +} + +// PaymentsResponse represents the JSON structure of the Flexpool API response for payments +type PaymentsResponse struct { + Error string `json:"error"` + Result struct { + Data []struct { + Hash string `json:"hash"` + Value int64 `json:"value"` + Timestamp int64 `json:"timestamp"` + } `json:"data"` + } `json:"result"` } // 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)) + 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 } - for _, result := range response["data"].([]interface{}) { - raw := result.(map[string]interface{}) + var response PaymentsResponse + json.Unmarshal(body, &response) + + for _, result := range response.Result.Data { payment := NewPayment( - raw["hash"].(string), - raw["value"].(float64), - raw["timestamp"].(float64), + result.Hash, + result.Value, + result.Timestamp, ) payments = append(payments, payment) if len(payments) >= limit { @@ -144,7 +140,7 @@ type WorkersResponse struct { // MinerWorkers returns a list of workers given a miner address func (f *FlexpoolClient) MinerWorkers(coin string, address string) (workers []*Worker, err error) { - body, err := f.requestBytes(fmt.Sprintf("%s/miner/workers?coin=%s&address=%s", FlexpoolAPIURL, coin, address)) + body, err := f.request(fmt.Sprintf("%s/miner/workers?coin=%s&address=%s", FlexpoolAPIURL, coin, address)) if err != nil { return nil, err } @@ -164,21 +160,35 @@ func (f *FlexpoolClient) MinerWorkers(coin string, address string) (workers []*W return workers, nil } +// BlocksResponse represents the JSON structure of the Flexpool API response for blocks +type BlocksResponse struct { + Error string `json:"error"` + Result struct { + Data []struct { + Hash string `json:"hash"` + Number int64 `json:"number"` + Reward int64 `json:"reward"` + } `json:"data"` + } `json:"result"` +} + // 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)) + body, 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{}) + var response BlocksResponse + json.Unmarshal(body, &response) + + for _, result := range response.Result.Data { block := NewBlock( - raw["hash"].(string), - raw["number"].(float64), - raw["reward"].(float64), + result.Hash, + result.Number, + result.Reward, ) blocks = append(blocks, block) if len(blocks) >= limit { diff --git a/main.go b/main.go index 78b0173..a43f262 100644 --- a/main.go +++ b/main.go @@ -132,7 +132,7 @@ func main() { log.Warnf("Could not fetch unpaid balance: %v", err) continue } - log.Debugf("Unpaid balance %.0f", balance) + log.Debugf("Unpaid balance %d", balance) miner.Balance = balance if miner.Balance != dbMiner.Balance { dbMiner.Balance = balance diff --git a/migrations/1.0_to_1.1.sql b/migrations/1.0_to_1.1.sql new file mode 100644 index 0000000..2a96630 --- /dev/null +++ b/migrations/1.0_to_1.1.sql @@ -0,0 +1,49 @@ +-- Migrate from 1.0 to 1.1 +-- SQLite doesn't support ALTER TABLE... ALTER TYPE +-- This migration file creates the new structures with "integer" types +-- instead of "real", move data to this new table then drop the old one. + +-- miners + +ALTER TABLE `miners` RENAME TO `miners_old`; + +CREATE TABLE `miners` ( + `id` integer, + `created_at` datetime, + `updated_at` datetime, + `deleted_at` datetime, + `coin` text, + `address` text NOT NULL UNIQUE, + `balance` integer, + `last_payment_timestamp` integer, + PRIMARY KEY (`id`) +); + +INSERT INTO `miners` SELECT * FROM `miners_old`; + +DROP TABLE `miners_old`; + +CREATE INDEX `idx_miners_deleted_at` ON `miners`(`deleted_at`); + + +-- pools + +ALTER TABLE `pools` RENAME TO `pools_old`; + +CREATE TABLE `pools` ( + `id` integer, + `created_at` datetime, + `updated_at` datetime, + `deleted_at` datetime, + `coin` text NOT NULL UNIQUE, + `last_block_number` integer, + PRIMARY KEY (`id`) +); + +INSERT INTO `pools` SELECT * FROM `pools_old`; + +DROP TABLE `pools_old`; + +CREATE INDEX `idx_pools_deleted_at` ON `pools`(`deleted_at`); + +SELECT "Database migrated to 1.1" \ No newline at end of file diff --git a/miner.go b/miner.go index a91d275..c32633b 100644 --- a/miner.go +++ b/miner.go @@ -19,8 +19,8 @@ type Miner struct { gorm.Model Coin string Address string `gorm:"unique;not null"` - Balance float64 - LastPaymentTimestamp float64 + Balance int64 + LastPaymentTimestamp int64 } // NewMiner creates a Miner @@ -56,12 +56,12 @@ func (m *Miner) String() string { // Payment to store payment attributes type Payment struct { Hash string - Value float64 - Timestamp float64 + Value int64 + Timestamp int64 } // NewPayment creates a Payment -func NewPayment(hash string, value float64, timestamp float64) *Payment { +func NewPayment(hash string, value int64, timestamp int64) *Payment { return &Payment{ Hash: hash, Value: value, diff --git a/notification.go b/notification.go index f2efd8f..9c6925f 100644 --- a/notification.go +++ b/notification.go @@ -119,7 +119,7 @@ func (t *TelegramNotifier) NotifyBlock(pool Pool, block Block) error { return err } - message := fmt.Sprintf("🎉 *%s* [#%.0f](%s) _%s_", verb, block.Number, url, ac.FormatMoney(convertedValue)) + message := fmt.Sprintf("🎉 *%s* [#%d](%s) _%s_", verb, block.Number, url, ac.FormatMoney(convertedValue)) return t.sendMessage(message) } diff --git a/pool.go b/pool.go index d69947e..b5f39c5 100644 --- a/pool.go +++ b/pool.go @@ -10,7 +10,7 @@ import ( type Pool struct { gorm.Model Coin string `gorm:"unique;not null"` - LastBlockNumber float64 + LastBlockNumber int64 } // NewPool creates a Pool @@ -25,13 +25,13 @@ func (p *Pool) String() string { // Block to store block attributes type Block struct { - Hash string `gorm:"unique;not null"` - Number float64 `gorm:"not null"` - Reward float64 `gorm:"not null"` + Hash string `gorm:"unique;not null"` + Number int64 `gorm:"not null"` + Reward int64 `gorm:"not null"` } // NewBlock creates a Block -func NewBlock(hash string, number float64, reward float64) *Block { +func NewBlock(hash string, number int64, reward int64) *Block { return &Block{ Hash: hash, Number: number, @@ -41,5 +41,5 @@ func NewBlock(hash string, number float64, reward float64) *Block { // String represents Block to a printable format func (b *Block) String() string { - return fmt.Sprintf("Block<%.0f>", b.Number) + return fmt.Sprintf("Block<%d>", b.Number) } diff --git a/utils.go b/utils.go index fa57434..1ccc9b8 100644 --- a/utils.go +++ b/utils.go @@ -12,7 +12,7 @@ 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) { +func ConvertCurrency(coin string, value int64) (float64, error) { switch coin { case "eth": return ConvertWeis(value), nil @@ -24,13 +24,13 @@ func ConvertCurrency(coin string, value float64) (float64, error) { } // ConvertWeis converts the value from Weis to ETH -func ConvertWeis(value float64) float64 { - return value / WeisToETHDivider +func ConvertWeis(value int64) float64 { + return float64(value) / WeisToETHDivider } // ConvertMojo converts the value from Mojo to XCH -func ConvertMojo(value float64) float64 { - return value / MojoToXCHDivider +func ConvertMojo(value int64) float64 { + return float64(value) / MojoToXCHDivider } // ConvertAction returns "Miner" for Ethereum and "Farmer" for Chia