Skip to content

Commit

Permalink
feat: add coinmarketcap provider
Browse files Browse the repository at this point in the history
  • Loading branch information
k-yang committed Sep 22, 2023
1 parent 104e0f1 commit beb5fd3
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 3 deletions.
5 changes: 2 additions & 3 deletions feeder/priceprovider/aggregateprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ type AggregatePriceProvider struct {
// given multiple PriceProvider.
func NewAggregatePriceProvider(
sourcesToPairSymbolMap map[string]map[asset.Pair]types.Symbol,
configToMap map[string]json.RawMessage,
sourceConfigMap map[string]json.RawMessage,
logger zerolog.Logger,
) types.PriceProvider {
providers := make(map[int]types.PriceProvider, len(sourcesToPairSymbolMap))
i := 0
for sourceName, pairToSymbolMap := range sourcesToPairSymbolMap {
providers[i] = NewPriceProvider(sourceName, pairToSymbolMap, configToMap[sourceName], logger)
providers[i] = NewPriceProvider(sourceName, pairToSymbolMap, sourceConfigMap[sourceName], logger)
i++
}

Expand All @@ -42,7 +42,6 @@ func NewAggregatePriceProvider(
// Iteration is exhaustive and random.
// If no correct PriceResponse is found, then an invalid PriceResponse is returned.
func (a AggregatePriceProvider) GetPrice(pair asset.Pair) types.Price {

// Temporarily treat NUSD as perfectly pegged to the US fiat dollar
// TODO(k-yang): add the NUSD pricefeed once it's available on exchanges
if pair.Equal(asset.Registry.Pair(denoms.NUSD, denoms.USD)) {
Expand Down
2 changes: 2 additions & 0 deletions feeder/priceprovider/priceprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func NewPriceProvider(
source = sources.NewTickSource(symbolsFromPairToSymbolMapping(pairToSymbolMap), sources.BinancePriceUpdate, logger)
case sources.Coingecko:
source = sources.NewTickSource(symbolsFromPairToSymbolMapping(pairToSymbolMap), sources.CoingeckoPriceUpdate(config), logger)
case sources.CoinMarketCap:
source = sources.NewTickSource(symbolsFromPairToSymbolMapping(pairToSymbolMap), sources.CoinmarketcapPriceUpdate(config), logger)
default:
panic("unknown price provider: " + sourceName)
}
Expand Down
135 changes: 135 additions & 0 deletions feeder/priceprovider/sources/coinmarketcap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Credit: @oleksandrmarkelov https://github.com/NibiruChain/pricefeeder/pull/27
package sources

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"github.com/NibiruChain/nibiru/x/common/set"
"github.com/NibiruChain/pricefeeder/types"
)

const (
CoinMarketCap = "coinmarketcap"
link = "https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest"
apiKeyHeaderParam = "X-CMC_PRO_API_KEY"
)

type CmcQuotePrice struct {
Price float64
}

type CmcQuote struct {
USD CmcQuotePrice
}

type CmcTicker struct {
Slug string
Quote CmcQuote
}

type CmcResponse struct {
Data map[string]CmcTicker
}

type CoinmarketcapConfig struct {
ApiKey string `json:"api_key"`
}

func CoinmarketcapPriceUpdate(rawConfig json.RawMessage) types.FetchPricesFunc {
return func(symbols set.Set[types.Symbol]) (map[types.Symbol]float64, error) {
config, err := getConfig(rawConfig)
if err != nil {
return nil, err
}

req, err := buildReq(symbols, config)
if err != nil {
return nil, err
}

client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()

response, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

rawPrices, err := getPricesFromResponse(symbols, response)
if err != nil {
return nil, err
}

return rawPrices, nil
}
}

// extractConfig tries to get the configuration, if nothing is found, it returns an empty config.
func getConfig(jsonConfig json.RawMessage) (*CoinmarketcapConfig, error) {
c := &CoinmarketcapConfig{}
if len(jsonConfig) > 0 {
err := json.Unmarshal(jsonConfig, c)
if err != nil {
return nil, fmt.Errorf("invalid coinmarketcap config: %w", err)
}
}
return c, nil
}

func getPricesFromResponse(symbols set.Set[types.Symbol], response []byte) (map[types.Symbol]float64, error) {
var respCmc CmcResponse
err := json.Unmarshal(response, &respCmc)
if err != nil {
return nil, err
}

cmcPrice := make(map[string]float64)
for _, value := range respCmc.Data {
cmcPrice[value.Slug] = value.Quote.USD.Price
}

rawPrices := make(map[types.Symbol]float64)
for symbol := range symbols {
if price, ok := cmcPrice[string(symbol)]; ok {
rawPrices[symbol] = price
} else {
return nil, fmt.Errorf("symbol %s not found in response: %s\n", symbol, response)
}
}

return rawPrices, err
}

func buildReq(symbols set.Set[types.Symbol], c *CoinmarketcapConfig) (*http.Request, error) {
req, err := http.NewRequest("GET", link, nil)
if err != nil {
return nil, fmt.Errorf("Can not create a request with link: %s\n", link)
}

params := url.Values{}
params.Add("slug", coinmarketcapSymbolCsv(symbols))

req.Header.Set("Accepts", "application/json")
req.Header.Add(apiKeyHeaderParam, c.ApiKey)
req.URL.RawQuery = params.Encode()

return req, nil
}

// coinmarketcapSymbolCsv returns the symbols as a comma separated string.
func coinmarketcapSymbolCsv(symbols set.Set[types.Symbol]) string {
s := ""
for symbol := range symbols {
s += string(symbol) + ","
}

return s[:len(s)-1]
}
34 changes: 34 additions & 0 deletions feeder/priceprovider/sources/coinmarketcap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package sources

import (
"encoding/json"
"testing"

"github.com/NibiruChain/nibiru/x/common/set"
"github.com/NibiruChain/pricefeeder/types"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/require"
)

func TestCoinmarketcapPriceUpdate(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

t.Run("success", func(t *testing.T) {
httpmock.RegisterResponder(
"GET", link+"?slug=bitcoin%2Cethereum",
httpmock.NewStringResponder(200, "{\"status\": {\"error_code\":0},\"data\":{\"1\":{\"slug\":\"bitcoin\",\"quote\":{\"USD\":{\"price\":23829}}}, \"100\":{\"slug\":\"ethereum\",\"quote\":{\"USD\":{\"price\":1676.85}}}}}"),
)
rawPrices, err := CoinmarketcapPriceUpdate(json.RawMessage{})(
set.New[types.Symbol](
"bitcoin",
"ethereum",
),
)
require.NoError(t, err)

require.Equal(t, 2, len(rawPrices))
require.Equal(t, rawPrices["bitcoin"], 23829.0)
require.Equal(t, rawPrices["ethereum"], 1676.85)
})
}

0 comments on commit beb5fd3

Please sign in to comment.