diff --git a/pkg/exchange/bitget/bitgetapi/client.go b/pkg/exchange/bitget/bitgetapi/client.go new file mode 100644 index 000000000..b8de1bf53 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/client.go @@ -0,0 +1,162 @@ +package bitgetapi + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "net/http" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/util" +) + +const defaultHTTPTimeout = time.Second * 15 +const RestBaseURL = "https://api.bitget.com" +const PublicWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" +const PrivateWebSocketURL = "wss://ws.bitget.com/spot/v1/stream" + +type RestClient struct { + BaseURL *url.URL + client *http.Client + + Key, Secret, Passphrase string +} + +func NewClient() *RestClient { + u, err := url.Parse(RestBaseURL) + if err != nil { + panic(err) + } + + return &RestClient{ + BaseURL: u, + client: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + } +} + +func (c *RestClient) Auth(key, secret, passphrase string) { + c.Key = key + // pragma: allowlist nextline secret + c.Secret = secret + c.Passphrase = passphrase +} + +// NewRequest create new API request. Relative url can be provided in refURL. +func (c *RestClient) newRequest(ctx context.Context, method, refURL string, params url.Values, body []byte) (*http.Request, error) { + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params != nil { + rel.RawQuery = params.Encode() + } + + pathURL := c.BaseURL.ResolveReference(rel) + return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) +} + +// sendRequest sends the request to the API server and handle the response +func (c *RestClient) sendRequest(req *http.Request) (*util.Response, error) { + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + // newResponse reads the response body and return a new Response object + response, err := util.NewResponse(resp) + if err != nil { + return response, err + } + + // Check error, if there is an error, return the ErrorResponse struct type + if response.IsError() { + return response, errors.New(string(response.Body)) + } + + return response, nil +} + +// newAuthenticatedRequest creates new http request for authenticated routes. +func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + if len(c.Key) == 0 { + return nil, errors.New("empty api key") + } + + if len(c.Secret) == 0 { + return nil, errors.New("empty api secret") + } + + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params != nil { + rel.RawQuery = params.Encode() + } + + pathURL := c.BaseURL.ResolveReference(rel) + path := pathURL.Path + if rel.RawQuery != "" { + path += "?" + rel.RawQuery + } + + // set location to UTC so that it outputs "2020-12-08T09:08:57.715Z" + t := time.Now().In(time.UTC) + timestamp := t.Format("2006-01-02T15:04:05.999Z07:00") + + var body []byte + + if payload != nil { + switch v := payload.(type) { + case string: + body = []byte(v) + + case []byte: + body = v + + default: + body, err = json.Marshal(v) + if err != nil { + return nil, err + } + } + } + + signKey := timestamp + strings.ToUpper(method) + path + string(body) + signature := Sign(signKey, c.Secret) + + req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + req.Header.Add("ACCESS-KEY", c.Key) + req.Header.Add("ACCESS-SIGN", signature) + req.Header.Add("ACCESS-TIMESTAMP", timestamp) + req.Header.Add("ACCESS-PASSPHRASE", c.Passphrase) + return req, nil +} + +func Sign(payload string, secret string) string { + var sig = hmac.New(sha256.New, []byte(secret)) + _, err := sig.Write([]byte(payload)) + if err != nil { + return "" + } + + return base64.StdEncoding.EncodeToString(sig.Sum(nil)) + // return hex.EncodeToString(sig.Sum(nil)) +} diff --git a/pkg/exchange/bitget/bitgetapi/types.go b/pkg/exchange/bitget/bitgetapi/types.go new file mode 100644 index 000000000..59153cb8d --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/types.go @@ -0,0 +1,36 @@ +package bitgetapi + +type SideType string + +const ( + SideTypeBuy SideType = "buy" + SideTypeSell SideType = "sell" +) + +type OrderType string + +const ( + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypePostOnly OrderType = "post_only" + OrderTypeFOK OrderType = "fok" + OrderTypeIOC OrderType = "ioc" +) + +type InstrumentType string + +const ( + InstrumentTypeSpot InstrumentType = "SPOT" + InstrumentTypeSwap InstrumentType = "SWAP" + InstrumentTypeFutures InstrumentType = "FUTURES" + InstrumentTypeOption InstrumentType = "OPTION" +) + +type OrderState string + +const ( + OrderStateCanceled OrderState = "canceled" + OrderStateLive OrderState = "live" + OrderStatePartiallyFilled OrderState = "partially_filled" + OrderStateFilled OrderState = "filled" +)