-
Notifications
You must be signed in to change notification settings - Fork 57
/
coingecko.go
130 lines (103 loc) · 3.3 KB
/
coingecko.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package coingecko
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/tokens"
)
const (
endpointSimplePrice = "/api/v3/simple/price"
defaultProdHost = "https://api.coingecko.com"
proProdHost = "https://pro-api.coingecko.com"
apikeyHeader = "x-cg-pro-api-key" //nolint:gosec // This is the header
)
type Client struct {
host string
apikey string
}
var _ tokens.Pricer = Client{}
// New creates a new coingecko Client with the given options.
func New(opts ...func(*options)) Client {
o := defaultOpts()
for _, opt := range opts {
opt(&o)
}
return Client{
host: o.Host,
apikey: o.APIKey,
}
}
// Price returns the price of each coin in USD.
func (c Client) Price(ctx context.Context, tkns ...tokens.Token) (map[tokens.Token]float64, error) {
return c.getPrice(ctx, "usd", tkns...)
}
// simplePriceResponse is the response from the simple/price endpoint.
// It mapes coin id to currency to price.
type simplePriceResponse map[string]map[string]float64
// GetPrice returns the price of each coin in the given currency.
func (c Client) getPrice(ctx context.Context, currency string, tkns ...tokens.Token) (map[tokens.Token]float64, error) {
ids := make([]string, len(tkns))
for i, t := range tkns {
ids[i] = t.CoingeckoID()
}
params := url.Values{
"ids": {strings.Join(ids, ",")},
"vs_currencies": {currency},
}
var resp simplePriceResponse
if err := c.doReq(ctx, endpointSimplePrice, params, &resp); err != nil {
return nil, errors.Wrap(err, "do req", "endpoint", "get_price")
}
prices := make(map[tokens.Token]float64)
for _, tkn := range tkns {
priceByCurrency, ok := resp[tkn.CoingeckoID()]
if !ok {
return nil, errors.New("missing token in response", "token", tkn)
}
price, ok := priceByCurrency[currency]
if !ok {
return nil, errors.New("missing price in response", "token", tkn, "currency", currency)
}
if price <= 0 {
return nil, errors.New("invalid price in response", "token", tkn, "price", price)
}
prices[tkn] = price
}
return prices, nil
}
// doReq makes a GET request to the given path & params, and decodes the response into response.
func (c Client) doReq(ctx context.Context, path string, params url.Values, response any) error {
uri, err := c.uri(path, params)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
if err != nil {
return errors.Wrap(err, "create request", "url", uri.String())
}
if c.apikey != "" {
req.Header.Set(apikeyHeader, c.apikey) //nolint:canonicalheader // As per CoinGacko docs
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return errors.Wrap(err, "get req", "url", uri.String())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("non-200 response", "url", uri.String(), "status", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(response); err != nil {
return errors.Wrap(err, "decode response")
}
return nil
}
func (c Client) uri(path string, params url.Values) (*url.URL, error) {
uri, err := url.Parse(c.host + path + "?" + params.Encode())
if err != nil {
return nil, errors.Wrap(err, "parse url", "host", c.host, "path", path, "params", params.Encode())
}
return uri, nil
}