· 4 years ago · Aug 25, 2021, 12:56 PM
1package shrimpy
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io/ioutil"
11 "net/http"
12 "strconv"
13 "strings"
14 "time"
15
16 "go.uber.org/zap"
17)
18
19// Shrimpy interface definition
20type Shrimpy interface {
21 GetAccounts() ([]AccountResponse, error)
22}
23
24// MustNewShrimpyPrediction gets a shrimpy prediction function
25func MustNewShrimpy(url, apiKey, apiSecret string, logger *zap.Logger) Shrimpy {
26 return &shrimpy{
27 baseURL: url,
28 apiKey: apiKey,
29 apiSecret: apiSecret,
30 logger: logger,
31 }
32}
33
34type shrimpy struct {
35 baseURL string
36 apiKey string
37 apiSecret string
38 logger *zap.Logger
39}
40
41// AccountResponse the JSON payload returned from shrimpy API account list
42type AccountResponse struct {
43 ID int `json:"id"`
44 Exchange string `json:"exchange"`
45 Isrebalancing bool `json:"isRebalancing"`
46}
47
48// GetAccounts retrieves a list of exchange accounts managed by shrimpy
49func (s *shrimpy) GetAccounts() (ret []AccountResponse, err error) {
50
51 // prepare the base request
52 url := fmt.Sprintf("%s/v1/accounts", s.baseURL)
53 s.logger.Debug("retrieving shrimpy accounts", zap.String("url", url))
54 req, err := http.NewRequest(http.MethodGet, url, nil)
55 if err != nil {
56 s.logger.Error("error generating request", zap.Error(err))
57 return nil, err
58 }
59 resp, err := s.doRequest(req, http.StatusOK, "")
60 if err != nil {
61 s.logger.Error("request error", zap.Error(err))
62 return nil, err
63 }
64
65 // handle response
66 defer resp.Body.Close()
67 body, _ := ioutil.ReadAll(resp.Body)
68 if err = json.Unmarshal(body, &ret); err != nil {
69 s.logger.Error("unable to parse response", zap.Error(err), zap.String("body", string(body)))
70 return
71 }
72 s.logger.Debug("successfully retrieved accounts", zap.Any("accounts", ret))
73 return
74}
75
76// doRequest prepare and send request to shrimpy API
77func (s *shrimpy) doRequest(req *http.Request, expectedRC int, body string) (*http.Response, error) {
78 now := time.Now()
79 nonce := strconv.FormatInt(now.Unix(), 10)
80
81 // create signature
82 signature, err := s.getSignature(req.URL.Path, req.Method, nonce, body)
83 if err != nil {
84 s.logger.Error("error generating signature", zap.Error(err))
85 return nil, err
86 }
87
88 // add required headers
89 req.Header.Add("Content-Type", "application/json")
90 req.Header.Add("SHRIMPY-API-KEY", s.apiKey)
91 req.Header.Add("SHRIMPY-API-NONCE", nonce)
92 req.Header.Add("SHRIMPY-API-SIGNATURE", signature)
93
94 // make the request
95 client := &http.Client{}
96 resp, err := client.Do(req)
97 if err != nil {
98 s.logger.Error("error retrieving shrimpy request", zap.Error(err))
99 return resp, err
100 }
101 if resp.StatusCode != expectedRC {
102 defer resp.Body.Close()
103 body, _ := ioutil.ReadAll(resp.Body)
104 msg := fmt.Sprintf("unexpected response code %d: %s", resp.StatusCode, string(body))
105 s.logger.Error(msg, zap.String("response", string(body)))
106 return resp, errors.New(msg)
107 }
108 s.logger.Debug("request successful")
109 return resp, nil
110}
111
112// getSignature generates a request signature
113func (s *shrimpy) getSignature(path, method, nonce, body string) (string, error) {
114 prehash := path + method + nonce + body
115 key, err := base64.StdEncoding.DecodeString(s.apiSecret)
116 if err != nil {
117 s.logger.Error("unable to decode secret", zap.Error(err))
118 return "", err
119 }
120 h := hmac.New(sha256.New, key)
121 if _, err = h.Write([]byte(prehash)); err != nil {
122 s.logger.Error("error writing prehash HMAC", zap.Error(err))
123 return "", err
124 }
125 signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
126 s.logger.Debug("successfully generated signature", zap.String("prehash", prehash), zap.String("signature", signature))
127 return signature, nil
128}