-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Created base bitfinex client and example usage
- Loading branch information
Showing
14 changed files
with
443 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package bitfinex | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/hmac" | ||
"crypto/sha512" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
) | ||
|
||
type Client struct { | ||
apiKey string | ||
apiSecret string | ||
publicApiEndpoint string | ||
privateApiEndpoint string | ||
httpClient *http.Client | ||
} | ||
|
||
const ( | ||
basePublicApiURL = "https://api-pub.bitfinex.com/v2" | ||
basePrivateApiURL = "https://api.bitfinex.com/v2" | ||
|
||
apiKeyHeader = "bfx-apikey" | ||
nonceHeader = "bfx-nonce" | ||
signatureHeader = "bfx-signature" | ||
) | ||
|
||
// NewClient initialize an API client instance with API key and secret key. | ||
// You should always call this function before using this SDK. | ||
// Services will be created by the form client.NewXXXService(). | ||
func NewClient(apiKey, apiSecret string) *Client { | ||
return &Client{ | ||
apiKey: apiKey, | ||
apiSecret: apiSecret, | ||
publicApiEndpoint: basePublicApiURL, | ||
privateApiEndpoint: basePrivateApiURL, | ||
httpClient: http.DefaultClient, | ||
} | ||
} | ||
|
||
func (c *Client) CallAPI(ctx context.Context, r *Request, opts ...RequestOption) (data []byte, err error) { | ||
// set request options from user | ||
for _, opt := range opts { | ||
opt(r) | ||
} | ||
|
||
req, err := c.getHttpRequest(ctx, r) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
res, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
data, err = io.ReadAll(res.Body) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
defer func() { | ||
cerr := res.Body.Close() | ||
// Only overwrite the retured error if the original error was nil and an | ||
// error occurred while closing the body. | ||
if err == nil && cerr != nil { | ||
err = cerr | ||
} | ||
}() | ||
|
||
if res.StatusCode >= http.StatusBadRequest { | ||
apiErr := &APIError{} | ||
e := json.Unmarshal(data, apiErr) | ||
if e != nil { | ||
fmt.Fprintf(os.Stderr, "failed to unmarshal json: %s from %s\n", e, string(data)) | ||
} | ||
return nil, apiErr | ||
} | ||
return data, nil | ||
} | ||
|
||
func (c *Client) getHttpRequest(ctx context.Context, r *Request) (*http.Request, error) { | ||
apiEndpoint := c.publicApiEndpoint | ||
path := fmt.Sprintf("%s%s", r.endpoint, r.subEndpoint) | ||
query := url.Values{} | ||
header := http.Header{} | ||
body := "" | ||
if r.method == http.MethodGet { | ||
for k, v := range r.params { | ||
query.Add(k, fmt.Sprintf("%v", v)) | ||
} | ||
if len(query) > 0 { | ||
path += "?" + query.Encode() | ||
} | ||
} else { | ||
b, err := json.Marshal(r.params) | ||
if err != nil { | ||
return nil, err | ||
} | ||
body = string(b) | ||
if body == "{}" { | ||
body = "" | ||
} | ||
header.Add("Content-Type", "application/json") | ||
header.Add("accept", "application/json") | ||
} | ||
if r.secType == SecTypePrivate { | ||
apiEndpoint = c.privateApiEndpoint | ||
nonce := currentTimestamp() | ||
header.Set(apiKeyHeader, c.apiKey) | ||
header.Set(nonceHeader, nonce) | ||
header.Set(signatureHeader, sign(c.apiSecret, fmt.Sprintf("/api/v2%s%s%s", path, nonce, body))) | ||
} | ||
// create request | ||
req, err := http.NewRequestWithContext(ctx, r.method, fmt.Sprintf("%s%s", apiEndpoint, path), bytes.NewBuffer([]byte(body))) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header = header | ||
return req, nil | ||
} | ||
|
||
func sign(secret, message string) string { | ||
mac := hmac.New(sha512.New384, []byte(secret)) | ||
mac.Write([]byte(message)) | ||
return hex.EncodeToString(mac.Sum(nil)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package bitfinex | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
) | ||
|
||
// APIError define API error | ||
// | ||
// Refer to https://binance-docs.github.io/apidocs/spot/en/#error-codes | ||
// for error code details | ||
type APIError []interface{} | ||
|
||
// Error return error code and message | ||
func (e APIError) Error() string { | ||
if len(e) == 3 { | ||
return fmt.Sprintf("<APIError> code=%v, msg=\"%v\"", e[1], e[2]) | ||
} | ||
b, _ := json.Marshal(e) | ||
return fmt.Sprintf("<APIError> msg=%s", string(b)) | ||
} | ||
|
||
// IsAPIError check if e is an API error | ||
func IsAPIError(e error) bool { | ||
_, ok := e.(*APIError) | ||
return ok | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package bitfinex | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestError(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
e APIError | ||
want string | ||
}{ | ||
{ | ||
name: "empty", | ||
e: APIError{}, | ||
want: "<APIError> msg=[]", | ||
}, | ||
{ | ||
name: "non-empty", | ||
e: APIError{"error", 10000, "error: test message"}, | ||
want: "<APIError> code=10000, msg=\"error: test message\"", | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
got := test.e.Error() | ||
if diff := cmp.Diff(got, test.want); diff != "" { | ||
t.Errorf("Error() got unexpected result: (-got +want)\n%s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestIsAPIError(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
e error | ||
want bool | ||
}{ | ||
{ | ||
name: "nil", | ||
e: nil, | ||
want: false, | ||
}, | ||
{ | ||
name: "APIError", | ||
e: &APIError{}, | ||
want: true, | ||
}, | ||
{ | ||
name: "other", | ||
e: fmt.Errorf("test"), | ||
want: false, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
got := IsAPIError(test.e) | ||
if got != test.want { | ||
t.Errorf("IsAPIError() = %v, want %v", got, test.want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/dasbd72/go-exchange-sdk/bitfinex | ||
|
||
go 1.22.0 | ||
|
||
require github.com/google/go-cmp v0.6.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package bitfinex | ||
|
||
import "net/http" | ||
|
||
type SecType int | ||
|
||
const ( | ||
// SecTypePublic is for public API | ||
SecTypePublic SecType = iota | ||
// SecTypePrivate is for private API | ||
SecTypePrivate | ||
) | ||
|
||
// Request define an API request, build with Request_builder | ||
type Request struct { | ||
method string | ||
endpoint string | ||
subEndpoint string | ||
secType SecType | ||
params map[string]interface{} | ||
} | ||
|
||
// Request_builder define a builder for Request | ||
type Request_builder struct { | ||
Method string | ||
Endpoint string | ||
SubEndpoint string | ||
SecType SecType | ||
Params map[string]interface{} | ||
} | ||
|
||
// Build create a new Request | ||
func (b Request_builder) Build() *Request { | ||
if b.Method == "" { | ||
b.Method = http.MethodGet | ||
} | ||
if b.Params == nil { | ||
b.Params = map[string]interface{}{} | ||
} | ||
return &Request{ | ||
method: b.Method, | ||
endpoint: b.Endpoint, | ||
subEndpoint: b.SubEndpoint, | ||
secType: b.SecType, | ||
params: b.Params, | ||
} | ||
} | ||
|
||
// RequestOption define option type for request | ||
type RequestOption func(*Request) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package bitfinex | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
func TestBuild(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
b Request_builder | ||
want *Request | ||
}{ | ||
{ | ||
name: "empty", | ||
b: Request_builder{}, | ||
want: &Request{ | ||
method: http.MethodGet, | ||
secType: SecTypePublic, | ||
params: map[string]interface{}{}, | ||
}, | ||
}, | ||
{ | ||
name: "non-empty", | ||
b: Request_builder{ | ||
Method: http.MethodPost, | ||
Endpoint: "/test", | ||
SubEndpoint: "/BTCUSD", | ||
SecType: SecTypePrivate, | ||
Params: map[string]interface{}{"test": "test"}, | ||
}, | ||
want: &Request{ | ||
method: http.MethodPost, | ||
endpoint: "/test", | ||
subEndpoint: "/BTCUSD", | ||
secType: SecTypePrivate, | ||
params: map[string]interface{}{"test": "test"}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
got := test.b.Build() | ||
if diff := cmp.Diff(got, test.want, cmp.AllowUnexported(Request{})); diff != "" { | ||
t.Errorf("Build() got unexpected result: (-got, +want)\n%s", diff) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package bitfinex | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
) | ||
|
||
func currentTimestamp() string { | ||
return fmt.Sprintf("%d", formatTimestamp(time.Now())) | ||
} | ||
|
||
// formatTimestamp formats a time into Unix timestamp in milliseconds, as requested by Binance. | ||
func formatTimestamp(t time.Time) int64 { | ||
return t.UnixNano() / int64(time.Millisecond) | ||
} |
Oops, something went wrong.