package main import ( "encoding/json" "fmt" "io/ioutil" "math/rand" "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 in bytes func (f *FlexpoolClient) request(url string) ([]byte, 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 } var result map[string]interface{} json.Unmarshal(jsonBody, &result) if result["error"] != nil { return nil, fmt.Errorf("Flexpool API error: %s", result["error"].(string)) } 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 float64 `json:"balance"` } `json:"result"` } // MinerBalance returns the current unpaid balance func (f *FlexpoolClient) MinerBalance(coin string, address string) (float64, error) { body, err := f.request(fmt.Sprintf("%s/miner/balance?coin=%s&address=%s", FlexpoolAPIURL, coin, address)) if err != nil { return 0, err } 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 { TotalPages int `json:"totalPages"` Data []struct { Hash string `json:"hash"` Value float64 `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 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 } var response PaymentsResponse json.Unmarshal(body, &response) if totalPages == 0 { totalPages = response.Result.TotalPages } for _, result := range response.Result.Data { payment := NewPayment( result.Hash, result.Value, result.Timestamp, ) payments = append(payments, payment) if len(payments) >= limit { break } } page++ if page >= totalPages { break } } if page > MaxIterations { return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations) } // Sort by timestamp sort.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 type WorkersResponse struct { Error string `json:"error"` Result []struct { Name string `json:"name"` IsOnline bool `json:"isOnline"` LastSteen int64 `json:"lastSeen"` } `json:"result"` } // 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.request(fmt.Sprintf("%s/miner/workers?coin=%s&address=%s", FlexpoolAPIURL, coin, address)) if err != nil { return nil, err } var response WorkersResponse json.Unmarshal(body, &response) for _, result := range response.Result { worker := NewWorker( address, result.Name, result.IsOnline, time.Unix(result.LastSteen, 0), ) workers = append(workers, worker) } return workers, nil } // BlocksResponse represents the JSON structure of the Flexpool API response for blocks type BlocksResponse struct { Error string `json:"error"` Result struct { TotalPages int `json:"totalPages"` Data []struct { Hash string `json:"hash"` Number uint64 `json:"number"` Reward float64 `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 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 } var response BlocksResponse json.Unmarshal(body, &response) if totalPages == 0 { totalPages = response.Result.TotalPages } for _, result := range response.Result.Data { block := NewBlock( result.Hash, result.Number, result.Reward, ) blocks = append(blocks, block) if len(blocks) >= limit { break } } page++ if page >= totalPages { break } } if page > MaxIterations { return nil, fmt.Errorf("Max iterations of %d reached", MaxIterations) } // Sort by number sort.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 }