Skip to content
This repository has been archived by the owner on Jul 4, 2024. It is now read-only.

Commit

Permalink
add markets endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
larscom committed Dec 3, 2023
1 parent 5cd5b97 commit bced632
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 35 deletions.
16 changes: 16 additions & 0 deletions examples/http/market/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
"log"

"github.com/larscom/go-bitvavo/v2"
)

func main() {
client := bitvavo.NewHttpClient()
markets, err := client.GetMarkets()
if err != nil {
log.Fatal(err)
}
log.Println("Markets", markets)
}
83 changes: 52 additions & 31 deletions httpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ func httpGet[T any](
logDebug func(message string, args ...any),
config *authConfig,
) (T, error) {
reqUrl := util.IfOrElse(len(params) > 0, func() string { return fmt.Sprintf("%s?%s", url, params.Encode()) }, url)
req, _ := http.NewRequest("GET", reqUrl, nil)
req, _ := http.NewRequest("GET", createRequestUrl(url, params), nil)

return httpDo[T](req, updateRateLimit, updateRateLimitResetAt, logDebug, config)
}
Expand All @@ -47,8 +46,7 @@ func httpPost[T any](
return body, err
}

reqUrl := util.IfOrElse(len(params) > 0, func() string { return fmt.Sprintf("%s?%s", url, params.Encode()) }, url)
req, _ := http.NewRequest("POST", reqUrl, bytes.NewBuffer(payload))
req, _ := http.NewRequest("POST", createRequestUrl(url, params), bytes.NewBuffer(payload))
return httpDo[T](req, updateRateLimit, updateRateLimitResetAt, logDebug, config)
}

Expand All @@ -61,46 +59,30 @@ func httpDo[T any](
) (T, error) {
logDebug("executing request", "method", request.Method, "url", request.URL.String())

var data T
var empty T
if err := applyHeaders(request, config); err != nil {
return data, err
return empty, err
}

response, err := client.Do(request)
if err != nil {
return data, err
return empty, err
}
defer response.Body.Close()

for key, value := range response.Header {
if key == headerRatelimit {
if len(value) == 0 {
return data, fmt.Errorf("header: %s didn't contain a value", headerRatelimit)
}
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])))
}
if err := updateRateLimits(response, updateRateLimit, updateRateLimitResetAt); err != nil {
return empty, err
}

if response.StatusCode > http.StatusIMUsed {
bytes, err := io.ReadAll(response.Body)
if err != nil {
return data, err
}

var apiError *jsond.BitvavoErr
if err := json.Unmarshal(bytes, &apiError); err != nil {
return data, fmt.Errorf("did not get OK response, code=%d, body=%s", response.StatusCode, string(bytes))
}
return data, apiError
return empty, unwrapErr(response)
}

defer response.Body.Close()
return unwrapBody[T](response)
}

func unwrapBody[T any](response *http.Response) (T, error) {
var data T
bytes, err := io.ReadAll(response.Body)
if err != nil {
return data, err
Expand All @@ -113,6 +95,41 @@ func httpDo[T any](
return data, nil
}

func unwrapErr(response *http.Response) error {
bytes, err := io.ReadAll(response.Body)
if err != nil {
return err
}

var bitvavoErr *jsond.BitvavoErr
if err := json.Unmarshal(bytes, &bitvavoErr); err != nil {
return fmt.Errorf("did not get OK response, code=%d, body=%s", response.StatusCode, string(bytes))
}
return bitvavoErr
}

func updateRateLimits(
response *http.Response,
updateRateLimit func(ratelimit int64),
updateRateLimitResetAt func(resetAt time.Time),
) error {
for key, value := range response.Header {
if key == headerRatelimit {
if len(value) == 0 {
return fmt.Errorf("header: %s didn't contain a value", headerRatelimit)
}
updateRateLimit(util.MustInt64(value[0]))
}
if key == headerRatelimitResetAt {
if len(value) == 0 {
return fmt.Errorf("header: %s didn't contain a value", headerRatelimitResetAt)
}
updateRateLimitResetAt(time.UnixMilli(util.MustInt64(value[0])))
}
}
return nil
}

func applyHeaders(request *http.Request, config *authConfig) error {
if config == nil {
return nil
Expand All @@ -137,3 +154,7 @@ func applyHeaders(request *http.Request, config *authConfig) error {

return nil
}

func createRequestUrl(url string, params url.Values) string {
return util.IfOrElse(len(params) > 0, func() string { return fmt.Sprintf("%s?%s", url, params.Encode()) }, url)
}
36 changes: 36 additions & 0 deletions httpc/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync"
"time"

"github.com/larscom/go-bitvavo/v2/jsond"
"github.com/larscom/go-bitvavo/v2/log"
"github.com/larscom/go-bitvavo/v2/util"
)
Expand Down Expand Up @@ -38,12 +39,22 @@ type HttpClient interface {
GetTime() (int64, error)

// GetRateLimit returns the remaining rate limit.
//
// Default value: -1
GetRateLimit() int64

// GetRateLimitResetAt returns the time (local time) when the counter resets.
//
// Default value: time.Now()
GetRateLimitResetAt() time.Time

// GetMarkets returns the available markets with their status (trading,halted,auction) and
// available order types.
GetMarkets() ([]jsond.Market, error)

// GetMarkets returns the available markets with their status (trading,halted,auction) and
// available order types for a single market (e.g: ETH-EUR)
GetMarket(market string) (jsond.Market, error)
}

type Option func(*httpClient)
Expand Down Expand Up @@ -126,6 +137,31 @@ func (c *httpClient) GetTime() (int64, error) {
return int64(resp["time"]), nil
}

func (c *httpClient) GetMarkets() ([]jsond.Market, error) {
return httpGet[[]jsond.Market](
fmt.Sprintf("%s/markets", httpUrl),
make(url.Values),
c.updateRateLimit,
c.updateRateLimitResetAt,
c.logDebug,
nil,
)
}

func (c *httpClient) GetMarket(market string) (jsond.Market, error) {
params := make(url.Values)
params.Add("market", market)

return httpGet[jsond.Market](
fmt.Sprintf("%s/markets", httpUrl),
params,
c.updateRateLimit,
c.updateRateLimitResetAt,
c.logDebug,
nil,
)
}

func (c *httpClient) updateRateLimit(ratelimit int64) {
c.mu.Lock()
defer c.mu.Unlock()
Expand Down
8 changes: 4 additions & 4 deletions httpc/httpclientauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"fmt"
"time"

neturl "net/url"
"net/url"

"github.com/larscom/go-bitvavo/v2/jsond"
)

type HttpClientAuth interface {
// GetBalance returns the balance on the account.
// Optionally provide the symbol to filter for.
// Optionally provide the symbol to filter for in uppercase (e.g: ETH)
GetBalance(symbol ...string) ([]jsond.Balance, error)

// GetAccount returns trading volume and fees for account.
Expand Down Expand Up @@ -46,7 +46,7 @@ func newHttpClientAuth(
}

func (c *httpClientAuth) GetBalance(symbol ...string) ([]jsond.Balance, error) {
params := make(neturl.Values)
params := make(url.Values)
if len(symbol) > 0 {
params.Add("symbol", symbol[0])
}
Expand All @@ -63,7 +63,7 @@ func (c *httpClientAuth) GetBalance(symbol ...string) ([]jsond.Balance, error) {
func (c *httpClientAuth) GetAccount() (jsond.Account, error) {
return httpGet[jsond.Account](
fmt.Sprintf("%s/account", httpUrl),
make(neturl.Values),
make(url.Values),
c.updateRateLimit,
c.updateRateLimitResetAt,
c.logDebug,
Expand Down
80 changes: 80 additions & 0 deletions jsond/market.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package jsond

import (
"github.com/goccy/go-json"
"github.com/larscom/go-bitvavo/v2/util"
)

type Market struct {
// The market itself
Market string `json:"market"`

// Enum: "trading" | "halted" | "auction"
Status string `json:"status"`

// Base currency, found on the left side of the dash in market.
Base string `json:"base"`

// Quote currency, found on the right side of the dash in market.
Quote string `json:"quote"`

// Price precision determines how many significant digits are allowed. The rationale behind this is that for higher amounts, smaller price increments are less relevant.
// Examples of valid prices for precision 5 are: 100010, 11313, 7500.10, 7500.20, 500.12, 0.0012345.
// Examples of precision 6 are: 11313.1, 7500.11, 7500.25, 500.123, 0.00123456.
PricePrecision int64 `json:"pricePrecision"`

// The minimum amount in quote currency (amountQuote or amount * price) for valid orders.
MinOrderInBaseAsset float64 `json:"minOrderInBaseAsset"`

// The minimum amount in base currency for valid orders.
MinOrderInQuoteAsset float64 `json:"minOrderInQuoteAsset"`

// // The maximum amount in quote currency (amountQuote or amount * price) for valid orders.
MaxOrderInBaseAsset float64 `json:"maxOrderInBaseAsset"`

// The maximum amount in base currency for valid orders.
MaxOrderInQuoteAsset float64 `json:"maxOrderInQuoteAsset"`

// Allowed order types for this market.
OrderTypes []string `json:"orderTypes"`
}

func (m *Market) UnmarshalJSON(bytes []byte) error {
var j map[string]any

err := json.Unmarshal(bytes, &j)
if err != nil {
return err
}

var (
market = j["market"].(string)
status = j["status"].(string)
base = j["base"].(string)
quote = j["quote"].(string)
pricePrecision = j["pricePrecision"].(float64)
minOrderInBaseAsset = j["minOrderInBaseAsset"].(string)
minOrderInQuoteAsset = j["minOrderInQuoteAsset"].(string)
maxOrderInBaseAsset = j["maxOrderInBaseAsset"].(string)
maxOrderInQuoteAsset = j["maxOrderInQuoteAsset"].(string)
orderTypesAny = j["orderTypes"].([]any)
)

orderTypes := make([]string, len(orderTypesAny))
for i := 0; i < len(orderTypesAny); i++ {
orderTypes[i] = orderTypesAny[i].(string)
}

m.Market = market
m.Status = status
m.Base = base
m.Quote = quote
m.PricePrecision = int64(pricePrecision)
m.MinOrderInBaseAsset = util.MustFloat64(minOrderInBaseAsset)
m.MinOrderInQuoteAsset = util.MustFloat64(minOrderInQuoteAsset)
m.MaxOrderInBaseAsset = util.MustFloat64(maxOrderInBaseAsset)
m.MaxOrderInQuoteAsset = util.MustFloat64(maxOrderInQuoteAsset)
m.OrderTypes = orderTypes

return nil
}

0 comments on commit bced632

Please sign in to comment.