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

Updated aggregator package #188

Merged
merged 5 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aggregator/fetchers/baseFetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func newBaseFetcher() baseFetcher {
func (b *baseFetcher) normalizeQuoteName(quote string, fetcherName string) string {
if strings.Contains(quote, quoteUSDFiat) {
switch fetcherName {
case BinanceName, CryptocomName, HitbtcName, HuobiName, OkexName:
case BinanceName, CryptocomName, HitbtcName, HuobiName, OkxName:
return quoteUSDT
default:
return quoteUSDFiat
Expand Down
8 changes: 5 additions & 3 deletions aggregator/fetchers/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ const (
HuobiName = "Huobi"
// KrakenName defines the Kraken exchange name
KrakenName = "Kraken"
// OkexName defines the Okex exchange name
OkexName = "Okex"
// OkxName defines the Okx exchange name
OkxName = "Okx"
// XExchangeName defines the XExchange name
XExchangeName = "XExchange"
// EVMGasPriceStation defines an EVM gas station that will push gas prices as a full token pair price
EVMGasPriceStation = "EVM gas price station"
)

// ImplementedFetchers is the map of all implemented exchange fetchers
Expand All @@ -33,6 +35,6 @@ var ImplementedFetchers = map[string]struct{}{
HitbtcName: {},
HuobiName: {},
KrakenName: {},
OkexName: {},
OkxName: {},
XExchangeName: {},
}
15 changes: 8 additions & 7 deletions aggregator/fetchers/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package fetchers
import "errors"

var (
errInvalidResponseData = errors.New("invalid response data")
errInvalidFetcherName = errors.New("invalid fetcher name")
errNilResponseGetter = errors.New("nil response getter")
errNilGraphqlGetter = errors.New("nil graphql getter")
errNilXExchangeTokensMap = errors.New("nil xexchange tokens map")
errInvalidPair = errors.New("invalid pair")
errInvalidGraphqlResponse = errors.New("invalid graphql response")
errInvalidResponseData = errors.New("invalid response data")
errInvalidFetcherName = errors.New("invalid fetcher name")
errNilResponseGetter = errors.New("nil response getter")
errNilGraphqlGetter = errors.New("nil graphql getter")
errNilXExchangeTokensMap = errors.New("nil xexchange tokens map")
errInvalidPair = errors.New("invalid pair")
errInvalidGraphqlResponse = errors.New("invalid graphql response")
errInvalidGasPriceSelector = errors.New("invalid gas price selector")
)
76 changes: 76 additions & 0 deletions aggregator/fetchers/evmGasPriceFetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package fetchers

import (
"context"
"fmt"

"github.com/multiversx/mx-sdk-go/aggregator"
)

const (
evmFastGasPrice = "FastGasPrice"
evmSafeGasPrice = "SafeGasPrice"
evmProposeGasPrice = "ProposeGasPrice"
)

// EVMGasPriceFetcherConfig represents the config DTO used for the gas price fetcher
type EVMGasPriceFetcherConfig struct {
ApiURL string
Selector string
}

type gasStationResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Result struct {
LastBlock string `json:"LastBlock"`
SafeGasPrice string `json:"SafeGasPrice"`
ProposeGasPrice string `json:"ProposeGasPrice"`
FastGasPrice string `json:"FastGasPrice"`
SuggestBaseFee string `json:"suggestBaseFee"`
GasUsedRatio string `json:"gasUsedRatio"`
} `json:"result"`
}

type evmGasPriceFetcher struct {
aggregator.ResponseGetter
config EVMGasPriceFetcherConfig
baseFetcher
}

// FetchPrice will fetch the price using the http client
func (fetcher *evmGasPriceFetcher) FetchPrice(ctx context.Context, base string, quote string) (float64, error) {
if !fetcher.hasPair(base, quote) {
return 0, aggregator.ErrPairNotSupported
}

response := &gasStationResponse{}
err := fetcher.ResponseGetter.Get(ctx, fmt.Sprintf(fetcher.config.ApiURL), response)
if err != nil {
return 0, err
}

latestGasPrice := 0
switch fetcher.config.Selector {
case evmFastGasPrice:
_, err = fmt.Sscanf(response.Result.FastGasPrice, "%d", &latestGasPrice)
case evmProposeGasPrice:
_, err = fmt.Sscanf(response.Result.ProposeGasPrice, "%d", &latestGasPrice)
case evmSafeGasPrice:
_, err = fmt.Sscanf(response.Result.SafeGasPrice, "%d", &latestGasPrice)
default:
err = fmt.Errorf("%w: %q", errInvalidGasPriceSelector, fetcher.config.Selector)
}

return float64(latestGasPrice), err
}

// Name returns the name
func (fetcher *evmGasPriceFetcher) Name() string {
return fmt.Sprintf("%s when using selector %s", EVMGasPriceStation, fetcher.config.Selector)
}

// IsInterfaceNil returns true if there is no value under the interface
func (fetcher *evmGasPriceFetcher) IsInterfaceNil() bool {
return fetcher == nil
}
135 changes: 135 additions & 0 deletions aggregator/fetchers/evmGasPriceFetcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package fetchers

import (
"context"
"errors"
"testing"

"github.com/multiversx/mx-sdk-go/aggregator"
"github.com/multiversx/mx-sdk-go/aggregator/mock"
"github.com/stretchr/testify/assert"
)

func createMockEVMGasPriceFetcher() *evmGasPriceFetcher {
return &evmGasPriceFetcher{
ResponseGetter: &mock.HttpResponseGetterStub{},
config: EVMGasPriceFetcherConfig{
ApiURL: "api-url",
Selector: "SafeGasPrice",
},
baseFetcher: newBaseFetcher(),
}
}

func TestEvmGasPriceFetcher_IsInterfaceNil(t *testing.T) {
t.Parallel()

var instance *evmGasPriceFetcher
assert.True(t, instance.IsInterfaceNil())

instance = &evmGasPriceFetcher{}
assert.False(t, instance.IsInterfaceNil())
}

func TestEvmGasPriceFetcher_Name(t *testing.T) {
t.Parallel()

fetcher := &evmGasPriceFetcher{
config: EVMGasPriceFetcherConfig{
Selector: "test selector",
},
}

assert.Equal(t, "EVM gas price station when using selector test selector", fetcher.Name())
}

func TestKraken_FetchPrice(t *testing.T) {
t.Parallel()

expectedErr := errors.New("expected error")
t.Run("un-added pair should error", func(t *testing.T) {
t.Parallel()

fetcher := createMockEVMGasPriceFetcher()
value, err := fetcher.FetchPrice(context.Background(), "missing", "pair")
assert.Zero(t, value)
assert.Equal(t, aggregator.ErrPairNotSupported, err)
})
t.Run("HTTP getter fails, should error", func(t *testing.T) {
t.Parallel()

fetcher := createMockEVMGasPriceFetcher()
fetcher.AddPair("test", "pair")
fetcher.ResponseGetter = &mock.HttpResponseGetterStub{
GetCalled: func(ctx context.Context, url string, response interface{}) error {
assert.Equal(t, "api-url", url)
return expectedErr
},
}
value, err := fetcher.FetchPrice(context.Background(), "test", "pair")
assert.Zero(t, value)
assert.Equal(t, expectedErr, err)
})
t.Run("invalid selector, should error", func(t *testing.T) {
t.Parallel()

fetcher := createMockEVMGasPriceFetcher()
fetcher.config.Selector = "invalid-selector"
fetcher.AddPair("test", "pair")
fetcher.ResponseGetter = &mock.HttpResponseGetterStub{
GetCalled: func(ctx context.Context, url string, response interface{}) error {
assert.Equal(t, "api-url", url)
return nil
},
}
value, err := fetcher.FetchPrice(context.Background(), "test", "pair")
assert.Zero(t, value)
assert.ErrorIs(t, err, errInvalidGasPriceSelector)
assert.Contains(t, err.Error(), "invalid-selector")
})
testHTTPResponseGetter := &mock.HttpResponseGetterStub{
GetCalled: func(ctx context.Context, url string, response interface{}) error {
assert.Equal(t, "api-url", url)
gasStationResp := response.(*gasStationResponse)
gasStationResp.Result.SafeGasPrice = "37"
gasStationResp.Result.ProposeGasPrice = "38"
gasStationResp.Result.FastGasPrice = "39"

return nil
},
}

t.Run("with SafeGasPrice should work", func(t *testing.T) {
t.Parallel()

fetcher := createMockEVMGasPriceFetcher()
fetcher.config.Selector = evmSafeGasPrice
fetcher.AddPair("test", "pair")
fetcher.ResponseGetter = testHTTPResponseGetter
value, err := fetcher.FetchPrice(context.Background(), "test", "pair")
assert.Nil(t, err)
assert.Equal(t, float64(37), value)
})
t.Run("with ProposeGasPrice should work", func(t *testing.T) {
t.Parallel()

fetcher := createMockEVMGasPriceFetcher()
fetcher.config.Selector = evmProposeGasPrice
fetcher.AddPair("test", "pair")
fetcher.ResponseGetter = testHTTPResponseGetter
value, err := fetcher.FetchPrice(context.Background(), "test", "pair")
assert.Nil(t, err)
assert.Equal(t, float64(38), value)
})
t.Run("with FastGasPrice should work", func(t *testing.T) {
t.Parallel()

fetcher := createMockEVMGasPriceFetcher()
fetcher.config.Selector = evmFastGasPrice
fetcher.AddPair("test", "pair")
fetcher.ResponseGetter = testHTTPResponseGetter
value, err := fetcher.FetchPrice(context.Background(), "test", "pair")
assert.Nil(t, err)
assert.Equal(t, float64(39), value)
})
}
55 changes: 35 additions & 20 deletions aggregator/fetchers/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,69 +12,84 @@ type XExchangeTokensPair struct {
Quote string
}

// ArgsPriceFetcher represents the arguments for the NewPriceFetcher function
type ArgsPriceFetcher struct {
FetcherName string
ResponseGetter aggregator.ResponseGetter
GraphqlGetter aggregator.GraphqlGetter
XExchangeTokensMap map[string]XExchangeTokensPair
EVMGasConfig EVMGasPriceFetcherConfig
}

// NewPriceFetcher returns a new price fetcher of the type provided
func NewPriceFetcher(fetcherName string, responseGetter aggregator.ResponseGetter, graphqlGetter aggregator.GraphqlGetter, xExchangeTokensMap map[string]XExchangeTokensPair) (aggregator.PriceFetcher, error) {
if responseGetter == nil {
func NewPriceFetcher(args ArgsPriceFetcher) (aggregator.PriceFetcher, error) {
if args.ResponseGetter == nil {
return nil, errNilResponseGetter
}
if graphqlGetter == nil {
if args.GraphqlGetter == nil {
return nil, errNilGraphqlGetter
}
if xExchangeTokensMap == nil && fetcherName == XExchangeName {
if args.XExchangeTokensMap == nil && args.FetcherName == XExchangeName {
return nil, errNilXExchangeTokensMap
}

return createFetcher(fetcherName, responseGetter, graphqlGetter, xExchangeTokensMap)
return createFetcher(args)
}

func createFetcher(fetcherName string, responseGetter aggregator.ResponseGetter, graphqlGetter aggregator.GraphqlGetter, xExchangeTokensMap map[string]XExchangeTokensPair) (aggregator.PriceFetcher, error) {
switch fetcherName {
func createFetcher(args ArgsPriceFetcher) (aggregator.PriceFetcher, error) {
switch args.FetcherName {
case BinanceName:
return &binance{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case BitfinexName:
return &bitfinex{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case CryptocomName:
return &cryptocom{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case GeminiName:
return &gemini{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case HitbtcName:
return &hitbtc{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case HuobiName:
return &huobi{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case KrakenName:
return &kraken{
ResponseGetter: responseGetter,
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case OkexName:
return &okex{
ResponseGetter: responseGetter,
case OkxName:
return &okx{
ResponseGetter: args.ResponseGetter,
baseFetcher: newBaseFetcher(),
}, nil
case XExchangeName:
return &xExchange{
GraphqlGetter: graphqlGetter,
GraphqlGetter: args.GraphqlGetter,
baseFetcher: newBaseFetcher(),
xExchangeTokensMap: xExchangeTokensMap,
xExchangeTokensMap: args.XExchangeTokensMap,
}, nil
case EVMGasPriceStation:
return &evmGasPriceFetcher{
ResponseGetter: args.ResponseGetter,
config: args.EVMGasConfig,
baseFetcher: newBaseFetcher(),
}, nil
}
return nil, fmt.Errorf("%w, fetcherName %s", errInvalidFetcherName, fetcherName)
return nil, fmt.Errorf("%w, fetcherName %s", errInvalidFetcherName, args.FetcherName)
}
Loading
Loading