-
Notifications
You must be signed in to change notification settings - Fork 3
/
rest.go
162 lines (146 loc) · 4.68 KB
/
rest.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
package bitmex
import (
"bytes"
"encoding"
"errors"
"fmt"
"github.com/go-openapi/runtime"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/adampointer/go-bitmex/swagger/client"
"github.com/avast/retry-go"
rc "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus"
)
var defaultRetryOpts = []retry.Option{retry.Delay(500 * time.Millisecond), retry.Attempts(10)}
func customTextConsumer(defaultTextConsumer runtime.Consumer) runtime.Consumer {
return runtime.ConsumerFunc(func(reader io.Reader, data interface{}) error {
if d, ok := data.(encoding.TextUnmarshaler); ok {
return defaultTextConsumer.Consume(reader, d)
}
return nil
})
}
// NewBitmexClient creates a new REST client
func NewBitmexClient(config *ClientConfig) *client.BitMEX {
transportConfig := client.DefaultTransportConfig().
WithHost(config.HostUrl.Host).
WithBasePath(client.DefaultBasePath).
WithSchemes([]string{config.HostUrl.Scheme})
httpclient := newHttpClient(config)
transport := rc.NewWithClient(transportConfig.Host, transportConfig.BasePath, transportConfig.Schemes, httpclient)
transport.Consumers[runtime.HTMLMime] = customTextConsumer(transport.Consumers[runtime.HTMLMime])
transport.Consumers[runtime.TextMime] = customTextConsumer(transport.Consumers[runtime.TextMime])
transport.Debug = config.Debug
return client.New(transport, strfmt.Default)
}
// ClientConfig holds configuration data for the REST client
// Rather than using this directly you should generally use the NewClientConfig
// function and the builder functions
type ClientConfig struct {
HostUrl *url.URL
underlyingTransport http.RoundTripper
ApiKey, ApiSecret string
Debug bool
RetryOpts []retry.Option
}
// NewClientConfig returns a *ClientConfig with the default transport set and the default retry options
// Default retry is exponential backoff, 10 attempts with an initial delay of 500 milliseconds
func NewClientConfig() *ClientConfig {
return &ClientConfig{
underlyingTransport: http.DefaultTransport,
RetryOpts: defaultRetryOpts,
}
}
// WithURL sets the url to use e.g. https://testnet.bitmex.com
func (c *ClientConfig) WithURL(u string) *ClientConfig {
hostUrl, err := url.Parse(u)
if err != nil {
log.Fatalf("cannot parse url: %s", err)
}
c.HostUrl = hostUrl
return c
}
// WithAuth sets the credentials and is optional if you are exclusively using public endpoints
func (c *ClientConfig) WithAuth(apiKey, apiSecret string) *ClientConfig {
c.ApiKey = apiKey
c.ApiSecret = apiSecret
return c
}
// WithTransport allows you to override the underlying transport used by the custom RoundTripper
func (c *ClientConfig) WithTransport(t http.RoundTripper) *ClientConfig {
c.underlyingTransport = t
return c
}
// WithRetryOptions sets the request retry options, replacing the defaults
func (c *ClientConfig) WithRetryOptions(opts ...retry.Option) *ClientConfig {
c.RetryOpts = opts
return c
}
// WithRetryOption appends a retry option to the defaults
func (c *ClientConfig) WithRetryOption(opt retry.Option) *ClientConfig {
c.RetryOpts = append(c.RetryOpts, opt)
return c
}
type transport struct {
config *ClientConfig
underlyingTransport http.RoundTripper
}
// RoundTrip implements http.RoundTripper for transport
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
if len(t.config.ApiKey) != 0 {
path := req.URL.Path
if len(req.URL.Query()) > 0 {
path = fmt.Sprintf("%s?%s", path, req.URL.RawQuery)
}
var body []byte
var err error
if req.Body != nil {
body, err = ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
req.Body = ioutil.NopCloser(bytes.NewReader(body))
}
params := &sigParams{
method: req.Method,
path: path,
secret: t.config.ApiSecret,
body: string(body),
expires: expiryTime(),
}
sig, err := calculateSignature(params)
if err != nil {
return nil, err
}
req.Header.Add("api-expires", params.expiryString())
req.Header.Add("api-key", t.config.ApiKey)
req.Header.Add("api-signature", sig)
}
var res *http.Response
err := retry.Do(func() error {
var err error
res, err = t.underlyingTransport.RoundTrip(req)
if err != nil {
return err
}
if res.StatusCode == 429 {
log.Fatal("rate limiting - shutting down")
return retry.Unrecoverable(errors.New("rate limiting"))
}
if res.StatusCode == 503 {
return errors.New("http status 503")
}
return nil
}, t.config.RetryOpts...)
return res, err
}
func newHttpClient(config *ClientConfig) *http.Client {
transport := &transport{underlyingTransport: config.underlyingTransport, config: config}
h := &http.Client{Transport: transport}
return h
}