Skip to content

Commit

Permalink
feat: fin provider (#42)
Browse files Browse the repository at this point in the history
* Make Sure You Are Square With Your God Before Trying To Merge This
  • Loading branch information
adamewozniak authored Mar 9, 2023
1 parent 5ce6bf5 commit ef1b51e
Show file tree
Hide file tree
Showing 5 changed files with 542 additions and 0 deletions.
1 change: 1 addition & 0 deletions config/supported_assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var (
provider.ProviderCrypto: false,
provider.ProviderPolygon: true,
provider.ProviderMock: false,
provider.ProviderFin: false,
}

// SupportedQuotes defines a lookup table for which assets we support
Expand Down
3 changes: 3 additions & 0 deletions oracle/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@ func NewProvider(
case provider.ProviderPolygon:
return provider.NewPolygonProvider(ctx, logger, endpoint, providerPairs...)

case provider.ProviderFin:
return provider.NewFinProvider(endpoint), nil

case provider.ProviderMock:
return provider.NewMockProvider(), nil
}
Expand Down
313 changes: 313 additions & 0 deletions oracle/provider/fin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
package provider

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

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ojo-network/price-feeder/oracle/types"
)

const (
finRestURL = "https://api.kujira.app"
finPairsEndpoint = "/api/coingecko/pairs"
finTickersEndpoint = "/api/coingecko/tickers"
finCandlesEndpoint = "/api/trades/candles"
finCandleBinSizeMinutes = 5
finCandleWindowSizeHours = 240
)

var _ Provider = (*FinProvider)(nil)

type (
// FinProvider implements an Oracle provider for use with the
// json-based FIN API.
//
// It polls and caches all data once every 30 seconds.
FinProvider struct {
baseURL string
client *http.Client
}

FinTickers struct {
Tickers []FinTicker `json:"tickers"`
}
FinTicker struct {
Base string `json:"base_currency"`
Target string `json:"target_currency"`
Symbol string `json:"ticker_id"`
Price string `json:"last_price"`
Volume string `json:"base_volume"`
}

FinCandles struct {
Candles []FinCandle `json:"candles"`
}
FinCandle struct {
Bin string `json:"bin"`
Close string `json:"close"`
Volume string `json:"volume"`
}

FinPairs struct {
Pairs []FinPair `json:"pairs"`
}
FinPair struct {
Base string `json:"base"`
Target string `json:"target"`
Symbol string `json:"ticker_id"`
Address string `json:"pool_id"`
}
)

// NewFinProvider returns a new instance of the Fin Provider.
func NewFinProvider(endpoint Endpoint) *FinProvider {
if endpoint.Name == ProviderFin {
return &FinProvider{
baseURL: endpoint.Rest,
client: newDefaultHTTPClient(),
}
}
return &FinProvider{
baseURL: finRestURL,
client: newDefaultHTTPClient(),
}
}

// GetTickerPrices queries the FIN json API and returns with a
// map of string => types.TickerPrice.
func (p FinProvider) GetTickerPrices(pairs ...types.CurrencyPair) (
map[string]types.TickerPrice, error,
) {
path := fmt.Sprintf("%s%s", p.baseURL, finTickersEndpoint)
tickerResponse, err := p.client.Get(path)
if err != nil {
return nil, fmt.Errorf("FIN tickers request failed: %w", err)
}
defer tickerResponse.Body.Close()

tickerContent, err := io.ReadAll(tickerResponse.Body)
if err != nil {
return nil,
fmt.Errorf("FIN tickers response read failed: %w", err)
}
var tickers FinTickers
err = json.Unmarshal(tickerContent, &tickers)
if err != nil {
return nil,
fmt.Errorf("FIN tickers response unmarshal failed: %w", err)
}
return tickers.toTickerPrices(pairs)
}

// toTickerPrices takes a FinTickers object and returns a
// map of string => types.TickerPrice.
func (ft FinTickers) toTickerPrices(pairs []types.CurrencyPair) (
map[string]types.TickerPrice, error,
) {
tickerSymbolPairs := make(map[string]types.CurrencyPair, len(pairs))
for _, pair := range pairs {
tickerSymbolPairs[pair.Base+"_"+pair.Quote] = pair
}
tickerPrices := make(map[string]types.TickerPrice, len(pairs))
for _, ticker := range ft.Tickers {
pair, ok := tickerSymbolPairs[strings.ToUpper(ticker.Symbol)]
if !ok {
// skip tokens that are not requested
continue
}
_, ok = tickerPrices[pair.String()]
if ok {
return nil,
fmt.Errorf("FIN tickers response contained duplicate: %s", ticker.Symbol)
}

price, err := strToDec(ticker.Price)
if err != nil {
return nil, err
}
volume, err := strToDec(ticker.Volume)
if err != nil {
return nil, err
}
tickerPrices[pair.String()] = types.TickerPrice{
Price: price,
Volume: volume,
}
}

for _, pair := range pairs {
_, ok := tickerPrices[pair.String()]
if !ok {
return nil,
fmt.Errorf("FIN ticker price missing for pair: %s", pair.String())
}
}
return tickerPrices, nil
}

// GetCandlePrices queries the FIN json API for each pair,
// gets each set of candles, and returns with a map
// of string => []types.CandlePrice.
func (p FinProvider) GetCandlePrices(pairs ...types.CurrencyPair) (
map[string][]types.CandlePrice, error,
) {
pairAddresses, err := p.getFinPairAddresses()
if err != nil {
return nil,
fmt.Errorf("FIN pair addresses lookup failed: %w", err)
}

candlePricesPairs := make(map[string][]types.CandlePrice)
for _, pair := range pairs {
address, ok := pairAddresses[pair.String()]
if !ok {
return nil,
fmt.Errorf("FIN contract address lookup failed for pair: %s", pair.String())
}
candlePricesPairs[pair.String()] = []types.CandlePrice{}

windowEndTime := time.Now()
windowStartTime := windowEndTime.Add(-finCandleWindowSizeHours * time.Hour)
path := fmt.Sprintf("%s%s?contract=%s&precision=%d&from=%s&to=%s",
p.baseURL,
finCandlesEndpoint,
address,
finCandleBinSizeMinutes,
windowStartTime.Format(time.RFC3339),
windowEndTime.Format(time.RFC3339),
)
candlesResponse, err := p.client.Get(path)
if err != nil {
return nil, fmt.Errorf("FIN candles request failed: %w", err)
}
defer candlesResponse.Body.Close()

candlesContent, err := io.ReadAll(candlesResponse.Body)
if err != nil {
return nil, fmt.Errorf("FIN candles response read failed: %w", err)
}
var candles FinCandles
err = json.Unmarshal(candlesContent, &candles)
if err != nil {
return nil, fmt.Errorf("FIN candles response unmarshal failed: %w", err)
}

cp, err := candles.ToCandlePrice()
if err != nil {
return nil, err
}
candlePricesPairs[pair.String()] = cp
}
return candlePricesPairs, nil
}

// ToCandlePrice converts a FinCandles object to a []types.CandlePrice object.
func (fc FinCandles) ToCandlePrice() ([]types.CandlePrice, error) {
candlePrices := []types.CandlePrice{}
for _, candle := range fc.Candles {
timeStamp, err := binToTimeStamp(candle.Bin)
if err != nil {
return nil, fmt.Errorf("FIN candle timestamp failed to parse: %s", candle.Bin)
}

close, err := strToDec(candle.Close)
if err != nil {
return nil, err
}
volume, err := strToDec(candle.Volume)
if err != nil {
return nil, err
}
candlePrices = append(candlePrices, types.CandlePrice{
Price: close,
Volume: volume,
TimeStamp: timeStamp,
})
}

return candlePrices, nil
}

// GetAvailablePairs queries fin's pairs and returns a map of
// pair => empty struct.
func (p FinProvider) GetAvailablePairs() (map[string]struct{}, error) {
finPairs, err := p.getFinPairs()
if err != nil {
return nil, err
}
availablePairs := make(map[string]struct{}, len(finPairs.Pairs))
for _, pair := range finPairs.Pairs {
pair := types.CurrencyPair{
Base: strings.ToUpper(pair.Base),
Quote: strings.ToUpper(pair.Target),
}
availablePairs[pair.String()] = struct{}{}
}
return availablePairs, nil
}

// getFinPairs queries the fin json API for available pairs,
// parses it, and returns it.
func (p FinProvider) getFinPairs() (FinPairs, error) {
path := fmt.Sprintf("%s%s", p.baseURL, finPairsEndpoint)
pairsResponse, err := p.client.Get(path)
if err != nil {
return FinPairs{}, err
}
defer pairsResponse.Body.Close()
var pairs FinPairs
err = json.NewDecoder(pairsResponse.Body).Decode(&pairs)
if err != nil {
return FinPairs{}, err
}
return pairs, nil
}

// getFinPairAddresses queries the fin API for token pairs,
// and returns a map of [base+quote] => pool id address.
func (p FinProvider) getFinPairAddresses() (map[string]string, error) {
finPairs, err := p.getFinPairs()
if err != nil {
return nil, err
}
pairAddresses := make(map[string]string, len(finPairs.Pairs))
for _, pair := range finPairs.Pairs {
pairAddresses[strings.ToUpper(pair.Base+pair.Target)] = pair.Address
}
return pairAddresses, nil
}

// SubscribeCurrencyPairs performs a no-op since fin does not use websockets
func (p FinProvider) SubscribeCurrencyPairs(_ ...types.CurrencyPair) {}

// binToTimeStamp takes a bin time expressed in a string
// and converts it into a unix timestamp.
func binToTimeStamp(bin string) (int64, error) {
timeParsed, err := time.Parse(time.RFC3339, bin)
if err != nil {
return -1, err
}
return timeParsed.Unix(), nil
}

// strToDec converts fin provider's decimals as a string to sdk.Dec.
// It's necessary because the precision of the decimal returned
// by the API is greater than what's supported by sdk.Dec (18).
func strToDec(str string) (sdk.Dec, error) {
if strings.Contains(str, ".") {
split := strings.Split(str, ".")
if len(split[1]) > 18 {
str = split[0] + "." + split[1][0:18]
}
}
return sdk.NewDecFromStr(str)
}

// StartConnections performs a no-op since fin is not a websocket
// provider.
func (p FinProvider) StartConnections() {}
Loading

0 comments on commit ef1b51e

Please sign in to comment.