diff --git a/README.md b/README.md index a61e504..271d156 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ > Go **thread safe** client library for Bitvavo v2 (https://docs.bitvavo.com) -Go Bitvavo is a **thread safe** client written in GO to interact with the Bitvavo platform. For _now_, only websockets (read only) are supported, so you can listen to all events that occur on the bitvavo platform (e.g: candles, ticker, orders, fills, etc) +Go Bitvavo is a **thread safe** client written in GO to interact with the Bitvavo platform. For _now_, mostly websockets (read only) are supported, so you can listen to all events that occur on the bitvavo platform (e.g: candles, ticker, orders, fills, etc) + +The HTTP client (read / write) is limited at the moment, it'll will grow with more functionality over time. ## 📒 Features -- [x] WebSockets - -- Read only -- [ ] REST (_soon_) - -- Read / Write -- [ ] ... +- [x] WebSocket Client -- Read only (100%) +- [ ] Http Client (_soon_) -- Read / Write + - Not complete yet, will grow with more functionality over time. ## 🚀 Installation @@ -349,7 +349,7 @@ type FillEvent struct { You can enable debug logging by providing an option to the Websocket constructor ```go - ws, err := bitvavo.NewWsClient(bitvavo.WithDebug(true)) + ws, err := bitvavo.NewWsClient(wsc.WithDebug(true)) ``` ### Auto Reconnect @@ -357,5 +357,5 @@ You can enable debug logging by providing an option to the Websocket constructor You can disable auto reconnecting to the websocket by providing an option to the Websocket constructor ```go - ws, err := bitvavo.NewWsClient(bitvavo.WithAutoReconnect(false)) + ws, err := bitvavo.NewWsClient(wsc.WithAutoReconnect(false)) ``` diff --git a/examples/http/account/main.go b/examples/http/account/main.go index a2569ef..2707418 100644 --- a/examples/http/account/main.go +++ b/examples/http/account/main.go @@ -17,7 +17,7 @@ func main() { key = os.Getenv("API_KEY") secret = os.Getenv("API_SECRET") client = bitvavo.NewHttpClient(httpc.WithDebug(true)) - authClient = client.ToAuthClient(key, secret, 10000) + authClient = client.ToAuthClient(key, secret, 0) ) balance, err := authClient.GetBalance() @@ -31,4 +31,8 @@ func main() { log.Fatal(err) } log.Println("Account", account) + + ratelimit := client.GetRateLimit() + resetAt := client.GetRateLimitResetAt() + log.Println("RateLimit", ratelimit, "ResetAt", resetAt) } diff --git a/examples/http/time/main.go b/examples/http/time/main.go index 5a68d21..f67ddd7 100644 --- a/examples/http/time/main.go +++ b/examples/http/time/main.go @@ -8,7 +8,9 @@ import ( func main() { client := bitvavo.NewHttpClient() - v, _ := client.GetTime() - log.Println("Time", v) - + time, err := client.GetTime() + if err != nil { + log.Fatal(err) + } + log.Println("Time", time) } diff --git a/httpc/client.go b/httpc/client.go index d904476..eb09ab3 100644 --- a/httpc/client.go +++ b/httpc/client.go @@ -5,41 +5,57 @@ import ( "fmt" "io" "net/http" - "strconv" "strings" "time" "github.com/goccy/go-json" "github.com/larscom/go-bitvavo/v2/crypto" "github.com/larscom/go-bitvavo/v2/log" + "github.com/larscom/go-bitvavo/v2/util" ) var ( client = http.DefaultClient ) -func httpGet[T any](url string, updateRateLimit func(int), config *authConfig) (T, error) { +func httpGet[T any]( + url string, + updateRateLimit func(int64), + updateRateLimitResetAt func(time.Time), + config *authConfig, +) (T, error) { req, _ := http.NewRequest("GET", url, nil) - return httpDo[T](req, updateRateLimit, config) + return httpDo[T](req, updateRateLimit, updateRateLimitResetAt, config) } -func httpPost[T any](url string, body T, updateRateLimit func(int), config *authConfig) (T, error) { +func httpPost[T any]( + url string, + body T, + updateRateLimit func(int64), + updateRateLimitResetAt func(time.Time), + config *authConfig, +) (T, error) { payload, err := json.Marshal(body) if err != nil { return body, err } req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload)) - return httpDo[T](req, updateRateLimit, config) + return httpDo[T](req, updateRateLimit, updateRateLimitResetAt, config) } -func httpDo[T any](request *http.Request, updateRatelimit func(int), config *authConfig) (T, error) { +func httpDo[T any]( + request *http.Request, + updateRatelimit func(int64), + updateRateLimitResetAt func(time.Time), + config *authConfig, +) (T, error) { + log.Logger().Debug("executing request", "method", request.Method, "url", request.URL.String()) + var data T if err := applyHeaders(request, config); err != nil { return data, err } - log.Logger().Debug("executing request", "method", request.Method, "url", request.URL.String()) - response, err := client.Do(request) if err != nil { return data, err @@ -50,8 +66,13 @@ func httpDo[T any](request *http.Request, updateRatelimit func(int), config *aut if len(value) == 0 { return data, fmt.Errorf("header: %s didn't contain a value", headerRatelimit) } - ratelimit, _ := strconv.Atoi(value[0]) - updateRatelimit(ratelimit) + updateRatelimit(util.MustInt64(value[0])) + } + if key == headerRatelimitResetAt { + if len(value) == 0 { + return data, fmt.Errorf("header: %s didn't contain a value", headerRatelimitResetAt) + } + updateRateLimitResetAt(time.UnixMilli(util.MustInt64(value[0]))) } } diff --git a/httpc/httpclient.go b/httpc/httpclient.go index 2867f1d..35bb04c 100644 --- a/httpc/httpclient.go +++ b/httpc/httpclient.go @@ -3,20 +3,31 @@ package httpc import ( "fmt" "sync" + "time" ) const ( - httpUrl = "https://api.bitvavo.com/v2" - - headerRatelimit = "Bitvavo-Ratelimit-Remaining" - headerAccessKey = "Bitvavo-Access-Key" - headerAccessSignature = "Bitvavo-Access-Signature" - headerAccessTimestamp = "Bitvavo-Access-Timestamp" - headerAccessWindow = "Bitvavo-Access-Window" + httpUrl = "https://api.bitvavo.com/v2" + maxWindowTimeMs = 60000 + + headerRatelimit = "Bitvavo-Ratelimit-Remaining" + headerRatelimitResetAt = "Bitvavo-Ratelimit-Resetat" + headerAccessKey = "Bitvavo-Access-Key" + headerAccessSignature = "Bitvavo-Access-Signature" + headerAccessTimestamp = "Bitvavo-Access-Timestamp" + headerAccessWindow = "Bitvavo-Access-Window" ) +const DefaultWindowTimeMs = 10000 + type HttpClient interface { - // ToAuthClient returns a client for authenticated requests + // ToAuthClient returns a client for authenticated requests. + // You need to provide an apiKey and an apiSecret which you can create in the bitvavo dashboard. + // + // WindowTimeMs is the window that allows execution of your request. + // + // If you set the value to 0, the default value of 10000 will be set. + // Whenever you go higher than the max value of 60000 the value will be set to 60000. ToAuthClient(apiKey string, apiSecret string, windowTimeMs uint64) HttpClientAuth // GetTime returns the current server time in milliseconds since 1 Jan 1970 @@ -24,24 +35,29 @@ type HttpClient interface { // GetRateLimit returns the remaining rate limit. // Default value: -1 - GetRateLimit() int + GetRateLimit() int64 - // GetRateLimitResetTime() time.Time + // GetRateLimitResetAt returns the time (local time) when the counter resets. + // Default value: time.Now() + GetRateLimitResetAt() time.Time } type Option func(*httpClient) type httpClient struct { - mu sync.RWMutex - ratelimit int - debug bool + debug bool + + mu sync.RWMutex + ratelimit int64 + ratelimitResetAt time.Time authClient *httpClientAuth } func NewHttpClient(options ...Option) HttpClient { client := &httpClient{ - ratelimit: -1, + ratelimit: -1, + ratelimitResetAt: time.Now(), } for _, opt := range options { @@ -63,21 +79,32 @@ func (c *httpClient) ToAuthClient(apiKey string, apiSecret string, windowTimeMs if c.hasAuthClient() { return c.authClient } + + if windowTimeMs == 0 { + windowTimeMs = DefaultWindowTimeMs + } + if windowTimeMs > maxWindowTimeMs { + windowTimeMs = maxWindowTimeMs + } config := &authConfig{ windowTimeMs: windowTimeMs, apiKey: apiKey, apiSecret: apiSecret, } - c.authClient = newHttpClientAuth(c.updateRateLimit, config) + c.authClient = newHttpClientAuth(c.updateRateLimit, c.updateRateLimitResetAt, config) return c.authClient } -func (c *httpClient) GetRateLimit() int { +func (c *httpClient) GetRateLimit() int64 { return c.ratelimit } +func (c *httpClient) GetRateLimitResetAt() time.Time { + return c.ratelimitResetAt +} + func (c *httpClient) GetTime() (int64, error) { - resp, err := httpGet[map[string]float64](fmt.Sprintf("%s/time", httpUrl), c.updateRateLimit, nil) + resp, err := httpGet[map[string]float64](fmt.Sprintf("%s/time", httpUrl), c.updateRateLimit, c.updateRateLimitResetAt, nil) if err != nil { return 0, err } @@ -85,12 +112,18 @@ func (c *httpClient) GetTime() (int64, error) { return int64(resp["time"]), nil } -func (c *httpClient) updateRateLimit(ratelimit int) { +func (c *httpClient) updateRateLimit(ratelimit int64) { c.mu.Lock() defer c.mu.Unlock() c.ratelimit = ratelimit } +func (c *httpClient) updateRateLimitResetAt(resetAt time.Time) { + c.mu.Lock() + defer c.mu.Unlock() + c.ratelimitResetAt = resetAt +} + func (c *httpClient) hasAuthClient() bool { return c.authClient != nil } diff --git a/httpc/httpclientauth.go b/httpc/httpclientauth.go index 26c3e38..db71528 100644 --- a/httpc/httpclientauth.go +++ b/httpc/httpclientauth.go @@ -2,6 +2,7 @@ package httpc import ( "fmt" + "time" "github.com/larscom/go-bitvavo/v2/jsond" ) @@ -15,8 +16,9 @@ type HttpClientAuth interface { } type httpClientAuth struct { - config *authConfig - updateRateLimit func(int) + config *authConfig + updateRateLimit func(int64) + updateRateLimitResetAt func(time.Time) } type authConfig struct { @@ -25,17 +27,22 @@ type authConfig struct { windowTimeMs uint64 } -func newHttpClientAuth(updateRateLimit func(int), config *authConfig) *httpClientAuth { +func newHttpClientAuth( + updateRateLimit func(int64), + updateRateLimitResetAt func(time.Time), + config *authConfig, +) *httpClientAuth { return &httpClientAuth{ - updateRateLimit: updateRateLimit, - config: config, + updateRateLimit: updateRateLimit, + updateRateLimitResetAt: updateRateLimitResetAt, + config: config, } } func (c *httpClientAuth) GetBalance() ([]jsond.Balance, error) { - return httpGet[[]jsond.Balance](fmt.Sprintf("%s/balance", httpUrl), c.updateRateLimit, c.config) + return httpGet[[]jsond.Balance](fmt.Sprintf("%s/balance", httpUrl), c.updateRateLimit, c.updateRateLimitResetAt, c.config) } func (c *httpClientAuth) GetAccount() (jsond.Account, error) { - return httpGet[jsond.Account](fmt.Sprintf("%s/account", httpUrl), c.updateRateLimit, c.config) + return httpGet[jsond.Account](fmt.Sprintf("%s/account", httpUrl), c.updateRateLimit, c.updateRateLimitResetAt, c.config) } diff --git a/wsc/wsclient.go b/wsc/wsclient.go index 9018642..6f77a98 100644 --- a/wsc/wsclient.go +++ b/wsc/wsclient.go @@ -14,7 +14,6 @@ const ( wsUrl = "wss://ws.bitvavo.com/v2" readLimit = 655350 handshakeTimeout = 45 * time.Second - maxWindowTimeMs = 60000 ) type EventHandler[T any] interface {