Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [BLO-1444] Bitstamp API Provider #571

Merged
merged 14 commits into from
Jul 10, 2024
Merged
2 changes: 1 addition & 1 deletion cmd/constants/marketmaps/markets.go
Original file line number Diff line number Diff line change
Expand Up @@ -5224,7 +5224,7 @@ var (
}
},
{
"name": "bitstamp_ws",
"name": "bitstamp_api",
"off_chain_ticker": "hbarusd"
},
{
Expand Down
6 changes: 6 additions & 0 deletions cmd/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/skip-mev/slinky/oracle/constants"
"github.com/skip-mev/slinky/oracle/types"
binanceapi "github.com/skip-mev/slinky/providers/apis/binance"
bitstampapi "github.com/skip-mev/slinky/providers/apis/bitstamp"
coinbaseapi "github.com/skip-mev/slinky/providers/apis/coinbase"
"github.com/skip-mev/slinky/providers/apis/coingecko"
"github.com/skip-mev/slinky/providers/apis/coinmarketcap"
Expand Down Expand Up @@ -55,6 +56,11 @@ var (
API: binanceapi.DefaultNonUSAPIConfig,
Type: types.ConfigType,
},
{
Name: bitstampapi.Name,
API: bitstampapi.DefaultAPIConfig,
Type: types.ConfigType,
},
{
Name: coinbaseapi.Name,
API: coinbaseapi.DefaultAPIConfig,
Expand Down
1 change: 1 addition & 0 deletions docs/validator/validator-connect-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ required to fetch relevant data.
Currently supported API Providers:

- binance_api
- bitstamp_api
- coinbase_api
- coingecko_api
- gecko_terminal_api
Expand Down
4 changes: 2 additions & 2 deletions oracle/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ Sample configuration:
"type": "price_provider"
},
{
"name": "bitstamp_ws",
"name": "bitstamp_api",
"api": {
"enabled": false,
"timeout": 0,
Expand All @@ -445,7 +445,7 @@ Sample configuration:
"maxBufferSize": 1024,
"reconnectionTimeout": 10000000000,
"wss": "wss://ws.bitstamp.net",
"name": "bitstamp_ws",
"name": "bitstamp_api",
"readBufferSize": 0,
"writeBufferSize": 0,
"handshakeTimeout": 45000000000,
Expand Down
5 changes: 5 additions & 0 deletions providers/apis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ The current set of supported providers are:
* `curl https://api.binance.com/api/v3/exchangeInfo | jq`
* Check if a given market is supported:
* `curl https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT | jq`
* [Bitstamp](./bitstamp/README.md) - Bitstamp is a cryptocurrency exchange that provides a free API for fetching cryptocurrency data. Bitstamp is a **primary data source** for the oracle.
* Check all supported markets:
* `curl https://www.bitstamp.net/api/v2/trading-pairs-info/ | jq`
* Check if a given market is supported:
* `curl https://www.bitstamp.net/api/v2/ticker/{btcusd} | jq`
* [Coinbase](./coinbase/README.md) - Coinbase is a cryptocurrency exchange that provides a free API for fetching cryptocurrency data. Coinbase is a **primary data source** for the oracle.
* Check all supported markets:
* `curl https://api.exchange.coinbase.com/currencies | jq`
Expand Down
5 changes: 5 additions & 0 deletions providers/apis/bitstamp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Bitstamp Provider

## Overview

The Bitstamp provider is used to fetch the spot price for cryptocurrencies from the [Bitstamp API](https://www.bitstamp.net/api/). As standard, all clients can make 400 requests per second. There is a default limit threshold of 10,000 requests per 10 minutes in place.
120 changes: 120 additions & 0 deletions providers/apis/bitstamp/api_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package bitstamp

import (
"encoding/json"
"fmt"
"net/http"
"time"

providertypes "github.com/skip-mev/slinky/providers/types"

"github.com/skip-mev/slinky/pkg/math"

"github.com/skip-mev/slinky/oracle/config"
"github.com/skip-mev/slinky/oracle/types"
)

var _ types.PriceAPIDataHandler = (*APIHandler)(nil)

// APIHandler implements the PriceAPIDataHandler interface for Bitstamp.
// for more information about the Bitstamp API, refer to the following link:
// https://www.bitstamp.net/api/#section/What-is-API
type APIHandler struct {
// api is the config for the Bitstamp API.
api config.APIConfig
// cache maintains the latest set of tickers seen by the handler.
cache types.ProviderTickers
}

// NewAPIHandler returns a new Bitstamp PriceAPIDataHandler.
func NewAPIHandler(
api config.APIConfig,
) (types.PriceAPIDataHandler, error) {
if api.Name != Name {
return nil, fmt.Errorf("expected api config name %s, got %s", Name, api.Name)

Check warning on line 34 in providers/apis/bitstamp/api_handler.go

View check run for this annotation

Codecov / codecov/patch

providers/apis/bitstamp/api_handler.go#L34

Added line #L34 was not covered by tests
}

if !api.Enabled {
return nil, fmt.Errorf("api config for %s is not enabled", Name)

Check warning on line 38 in providers/apis/bitstamp/api_handler.go

View check run for this annotation

Codecov / codecov/patch

providers/apis/bitstamp/api_handler.go#L38

Added line #L38 was not covered by tests
}

if err := api.ValidateBasic(); err != nil {
return nil, fmt.Errorf("invalid api config for %s: %w", Name, err)

Check warning on line 42 in providers/apis/bitstamp/api_handler.go

View check run for this annotation

Codecov / codecov/patch

providers/apis/bitstamp/api_handler.go#L42

Added line #L42 was not covered by tests
}

return &APIHandler{
api: api,
cache: types.NewProviderTickers(),
}, nil
}

// CreateURL returns the URL that is used to fetch data from the Bitstamp API for the
// given tickers.
func (h *APIHandler) CreateURL(
tickers []types.ProviderTicker,
) (string, error) {
if len(tickers) == 0 {
return "", fmt.Errorf("no tickers provided")
}

for _, ticker := range tickers {
h.cache.Add(ticker)
}

return h.api.Endpoints[0].URL, nil
}

// ParseResponse parses the response from the Bitstamp API and returns a GetResponse. Each
// of the tickers supplied will get a response or an error. Note that the bitstamp API
// returns all prices, some of which are not needed. The response is filtered to only
// include the prices that are needed (i.e. the pairs that are in the cache).
func (h *APIHandler) ParseResponse(
tickers []types.ProviderTicker,
resp *http.Response,
) types.PriceResponse {
var result MarketTickerResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return types.NewPriceResponseWithErr(
tickers,
providertypes.NewErrorWithCode(err, providertypes.ErrorFailedToDecode),
)
}

var (
resolved = make(types.ResolvedPrices)
unresolved = make(types.UnResolvedPrices)
)

for _, feed := range result {
// Filter out the responses that are not expected.
ticker, ok := h.cache.FromOffChainTicker(feed.Pair)
if !ok {
continue

Check warning on line 92 in providers/apis/bitstamp/api_handler.go

View check run for this annotation

Codecov / codecov/patch

providers/apis/bitstamp/api_handler.go#L92

Added line #L92 was not covered by tests
}

price, err := math.Float64StringToBigFloat(feed.Last)
if err != nil {
wErr := fmt.Errorf("failed to convert price %s to big.Float: %w", feed.Last, err)
unresolved[ticker] = providertypes.UnresolvedResult{
ErrorWithCode: providertypes.NewErrorWithCode(wErr, providertypes.ErrorFailedToParsePrice),

Check warning on line 99 in providers/apis/bitstamp/api_handler.go

View check run for this annotation

Codecov / codecov/patch

providers/apis/bitstamp/api_handler.go#L97-L99

Added lines #L97 - L99 were not covered by tests
}
continue

Check warning on line 101 in providers/apis/bitstamp/api_handler.go

View check run for this annotation

Codecov / codecov/patch

providers/apis/bitstamp/api_handler.go#L101

Added line #L101 was not covered by tests
}

resolved[ticker] = types.NewPriceResult(price, time.Now().UTC())
}

// Add currency pairs that received no response to the unresolved map.
for _, ticker := range tickers {
_, resolvedOk := resolved[ticker]
_, unresolvedOk := unresolved[ticker]

if !resolvedOk && !unresolvedOk {
unresolved[ticker] = providertypes.UnresolvedResult{
ErrorWithCode: providertypes.NewErrorWithCode(fmt.Errorf("no response"), providertypes.ErrorNoResponse),
}
}
}

return types.NewPriceResponse(resolved, unresolved)
}
Loading
Loading