-
Notifications
You must be signed in to change notification settings - Fork 3
/
main.go
192 lines (155 loc) · 4.75 KB
/
main.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package glassnode
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
const (
BaseURLV1 = "https://api.glassnode.com/v1/"
MetricsPrefix = "metrics"
)
func NewClient(apiKey string) *Client {
return &Client{
BaseURL: BaseURLV1,
apiKey: apiKey,
http: &http.Client{
Timeout: time.Minute,
},
}
}
type Client struct {
BaseURL string
apiKey string
http *http.Client
}
func (c *Client) sendRequest(req *http.Request) ([]byte, error) {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json; charset=utf-8")
res, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("[sendRequest] HTTP request unsuccessful: %d", res.StatusCode)
}
bResponse, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("[sendRequest] couldnt read response body: %s", err.Error())
}
return bResponse, nil
}
// APIOptionsList represents query parameters in a request
// more details under https://docs.glassnode.com/api/indicators
type APIOptionsList struct {
// Asset (required) indicates which asset you'd like to request, eg. BTC.
// stands for the a parameter, translates to: &a=BTC in the final URL
Asset string
// Metric (required) indicates which metric you'd like to request, eg. sopr.
// Represents part of the URL path, translates to: /sopr in the final URL
Metric string
// Category (required) is the URL modificator, can be one of: market, derivatives etc, check glassnode.
// Represents part of the URL path, translates to: /market/<metric> in the final URL
Category string
// DirectMapping forms a sql-like union with the other parameters, must follow Glassnode documentation.
// May be something like {"s": "123"} which adds &s=123 to the request
DirectMapping map[string]string
// Since is a UNIX Timestamp indicating the starting point for the fetched dataset.
// Stands for the s parameter, translates to: &s=<value> in the final URL
Since int
// Until is a UNIX Timestamp indicating the ending point for the fetched dataset.
// Stands for the u parameter, translates to: &u=<value> in the final URL
Until int
// Frequency specifies the data interval, usually 1h, 24h, check glassnode.
// Stands for the i parameter, translates to: &i=<value> in the final URL
Frequency string
// Format - defaults to JSON and that's the only one supported in this lib so far.
// Only here to indicate it's not supported.
// If you speficy "f" in DirectMappings, it'll be erased.
// Format string
}
func GetMetricData(ctx context.Context, api Client, options *APIOptionsList) (interface{}, error) {
// -----------------
// Parse API options
// -----------------
fullURL, err := constructURL(api, options)
if err != nil {
return nil, err
}
// -------------------
// Construct a request
// -------------------
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("[GetMetricData] error wrapping request: %s", err.Error())
}
req = req.WithContext(ctx)
res, err := api.sendRequest(req)
if err != nil {
return nil, fmt.Errorf("[GetMetricData] request errored: %s", err.Error())
}
// ----------------------
// Unmarshal the response
// ----------------------
parsedResponse, err := UnmarshalJSON(res)
if err != nil {
return nil, fmt.Errorf("[GetMetricData] error parsing response: %s", err.Error())
}
return parsedResponse, nil
}
// -----------
// Data models
// -----------
type TimeValue struct {
Time int64 `json:"t"`
Value float64 `json:"v"`
}
type TimeOptions struct {
Time int64 `json:"t"`
Options map[string]float64 `json:"o"`
}
// -----------------------
// Dual-type unmarshalling
// -----------------------
func UnmarshalJSON(b []byte) (interface{}, error) {
//
// Attempt the first type conversion
//
tv := []TimeValue{}
err := json.Unmarshal(b, &tv)
//
// no error, but we need to make sure we unmarshalled into the correct type
//
if err == nil && tv[0].Value != 0.0 {
// it appears to be the TimeValue type, return.
return tv, nil
}
// So it appears it's not the TimeValue type, now check for errors
// and attempt unmarshalling into TimeOptions
//
// abort if we have an error other than the wrong type
//
if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok {
return nil, err
}
//
// Unmarshalling into TimeOptions
//
to := []TimeOptions{}
err = json.Unmarshal(b, &to)
if err != nil {
return nil, err
}
return to, nil
}
// -----
// Utils
// -----
// YesterdayTimestamp returns an int timestamp of the current time minus 24h
func YesterdayTimestamp() int {
dt := time.Now().Add(-24 * time.Hour)
return int(dt.Unix())
}