From 3bfc8b934ff621b1c17545cec48fce70842ebcef Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:03:26 -0700 Subject: [PATCH 01/10] add mexc provider --- price-feeder/config/config.go | 1 + price-feeder/oracle/oracle.go | 3 + price-feeder/oracle/provider/mexc.go | 520 ++++++++++++++++++++++ price-feeder/oracle/provider/mexc_test.go | 100 +++++ price-feeder/oracle/provider/provider.go | 1 + 5 files changed, 625 insertions(+) create mode 100644 price-feeder/oracle/provider/mexc.go create mode 100644 price-feeder/oracle/provider/mexc_test.go diff --git a/price-feeder/config/config.go b/price-feeder/config/config.go index 2189562c93..acc372cb21 100644 --- a/price-feeder/config/config.go +++ b/price-feeder/config/config.go @@ -40,6 +40,7 @@ var ( provider.ProviderCoinbase: {}, provider.ProviderFTX: {}, provider.ProviderBitget: {}, + provider.ProviderMexc: {}, provider.ProviderMock: {}, } diff --git a/price-feeder/oracle/oracle.go b/price-feeder/oracle/oracle.go index fc84c997b2..e3b237d2b6 100644 --- a/price-feeder/oracle/oracle.go +++ b/price-feeder/oracle/oracle.go @@ -472,6 +472,9 @@ func NewProvider( case provider.ProviderBitget: return provider.NewBitgetProvider(ctx, logger, endpoint, providerPairs...) + case provider.ProviderMexc: + return provider.NewMexcProvider(ctx, logger, endpoint, providerPairs...) + case provider.ProviderMock: return provider.NewMockProvider(), nil } diff --git a/price-feeder/oracle/provider/mexc.go b/price-feeder/oracle/provider/mexc.go new file mode 100644 index 0000000000..a060d5df46 --- /dev/null +++ b/price-feeder/oracle/provider/mexc.go @@ -0,0 +1,520 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/gorilla/websocket" + "github.com/rs/zerolog" + "github.com/umee-network/umee/price-feeder/oracle/types" +) + +const ( + mexcWSHost = "wbs.mexc.com" + mexcWSPath = "/raw/ws" + mexcRestHost = "https://www.mexc.com" + mexcRestPath = "/open/api/v2/market/ticker" +) + +var _ Provider = (*MexcProvider)(nil) + +type ( + // MexcProvider defines an Oracle provider implemented by the Mexc public + // API. + // + // REF: https://mxcdevelop.github.io/apidocs/spot_v2_en/#ticker-information + // REF: https://mxcdevelop.github.io/apidocs/spot_v2_en/#k-line + // REF: https://mxcdevelop.github.io/apidocs/spot_v2_en/#overview + MexcProvider struct { + wsURL url.URL + wsClient *websocket.Conn + logger zerolog.Logger + mtx sync.RWMutex + endpoints Endpoint + tickers map[string]MexcTicker // Symbol => MexcTicker + candles map[string][]MexcCandle // Symbol => MexcCandle + subscribedPairs map[string]types.CurrencyPair // Symbol => types.CurrencyPair + } + + // MexcTicker ticker price response. https://pkg.go.dev/encoding/json#Unmarshal + // Unmarshal matches incoming object keys to the keys used by Marshal (either the + // struct field name or its tag), preferring an exact match but also accepting a + // case-insensitive match. C field which is Statistics close time is not used, but + // it avoids to implement specific UnmarshalJSON. + + MexcTicker struct { + Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT + LastPrice string `json:"p"` // Last price ex.: 0.0025 + Volume string `json:"v"` // Total traded base asset volume ex.: 1000 + C uint64 `json:"C"` // Statistics close time + } + + MexcTickerData struct { + LastPrice float64 `json:"p"` // Last price ex.: 0.0025 + Volume float64 `json:"v"` // Total traded base asset volume ex.: 1000 + } + + MexcTickerResult struct { + Channel string `json:"channel"` // expect "push.overview" + Symbol map[string]MexcTickerData `json:"data"` // this key is the Symbol ex.: ATOM_USDT + } + + // MexcCandleMetadata candle metadata used to compute tvwap price. + MexcCandleMetadata struct { + Close float64 `json:"c"` // Price at close + TimeStamp int64 `json:"t"` // Close time in unix epoch ex.: 1645756200000 + Volume float64 `json:"v"` // Volume during period + } + + // MexcCandle candle Mexc websocket channel "kline_1m" response. + MexcCandle struct { + // Channel string `json:"channel"` // expect "push.kline" + Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT + Metadata MexcCandleMetadata `json:"data"` // Metadata for candle + } + + // MexcSubscribeMsg Msg to subscribe all the tickers channels. + MexcCandleSubscriptionMsg struct { + OP string `json:"op"` // kline + Symbol string `json:"symbol"` // streams to subscribe ex.: atom_usdt + Interval string `json:"interval"` // Min1、Min5、Min15、Min30 + } + + // MexcSubscribeMsg Msg to subscribe all the tickers channels. + MexcTickerSubscriptionMsg struct { + OP string `json:"op"` // kline + } + + // MexcPairSummary defines the response structure for a Mexc pair + // summary. + MexcPairSummary struct { + Symbol string `json:"symbol"` + } +) + +func NewMexcProvider( + ctx context.Context, + logger zerolog.Logger, + endpoints Endpoint, + pairs ...types.CurrencyPair, +) (*MexcProvider, error) { + if (endpoints.Name) != ProviderMexc { + endpoints = Endpoint{ + Name: ProviderMexc, + Rest: mexcRestHost, + Websocket: mexcWSHost, + } + } + + wsURL := url.URL{ + Scheme: "wss", + Host: endpoints.Websocket, + Path: mexcWSPath, + } + + wsConn, resp, err := websocket.DefaultDialer.Dial(wsURL.String(), nil) + defer resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("error connecting to mexc websocket: %w", err) + } + + provider := &MexcProvider{ + wsURL: wsURL, + wsClient: wsConn, + logger: logger.With().Str("provider", "mexc").Logger(), + endpoints: endpoints, + tickers: map[string]MexcTicker{}, + candles: map[string][]MexcCandle{}, + subscribedPairs: map[string]types.CurrencyPair{}, + } + + if err := provider.SubscribeCurrencyPairs(pairs...); err != nil { + return nil, err + } + + go provider.handleWebSocketMsgs(ctx) + + return provider, nil +} + +// GetTickerPrices returns the tickerPrices based on the provided pairs. +func (p *MexcProvider) GetTickerPrices(pairs ...types.CurrencyPair) (map[string]types.TickerPrice, error) { + tickerPrices := make(map[string]types.TickerPrice, len(pairs)) + + for _, cp := range pairs { + key := currencyPairToMexcPair(cp) + price, err := p.getTickerPrice(key) + if err != nil { + return nil, err + } + tickerPrices[cp.String()] = price + } + + return tickerPrices, nil +} + +// GetCandlePrices returns the candlePrices based on the provided pairs. +func (p *MexcProvider) GetCandlePrices(pairs ...types.CurrencyPair) (map[string][]types.CandlePrice, error) { + candlePrices := make(map[string][]types.CandlePrice, len(pairs)) + + for _, cp := range pairs { + key := currencyPairToMexcPair(cp) + prices, err := p.getCandlePrices(key) + if err != nil { + return nil, err + } + candlePrices[cp.String()] = prices + } + + return candlePrices, nil +} + +// SubscribeCurrencyPairs subscribe all currency pairs into ticker and candle channels. +func (p *MexcProvider) SubscribeCurrencyPairs(cps ...types.CurrencyPair) error { + if len(cps) == 0 { + return fmt.Errorf("currency pairs is empty") + } + + if err := p.subscribeChannels(cps...); err != nil { + return err + } + + p.setSubscribedPairs(cps...) + return nil +} + +// subscribeChannels subscribe to the ticker and candle channels for all currency pairs. +func (p *MexcProvider) subscribeChannels(cps ...types.CurrencyPair) error { + if err := p.subscribeTickers(cps...); err != nil { + return err + } + + return p.subscribeCandles(cps...) +} + +// subscribeTickers subscribe to the ticker channel for all currency pairs. +func (p *MexcProvider) subscribeTickers(cps ...types.CurrencyPair) error { + pairs := make([]string, len(cps)) + + for i, cp := range cps { + pairs[i] = currencyPairToMexcPair(cp) + } + + return p.subscribePairs(pairs...) +} + +// subscribeCandles subscribe to the candle channel for all currency pairs. +func (p *MexcProvider) subscribeCandles(cps ...types.CurrencyPair) error { + pairs := make([]string, len(cps)) + + for i, cp := range cps { + pairs[i] = currencyPairToMexcPair(cp) + } + + return p.subscribePairs(pairs...) +} + +// subscribedPairsToSlice returns the map of subscribed pairs as a slice. +func (p *MexcProvider) subscribedPairsToSlice() []types.CurrencyPair { + p.mtx.RLock() + defer p.mtx.RUnlock() + + return types.MapPairsToSlice(p.subscribedPairs) +} + +func (p *MexcProvider) getTickerPrice(key string) (types.TickerPrice, error) { + p.mtx.RLock() + defer p.mtx.RUnlock() + + ticker, ok := p.tickers[key] + if !ok { + return types.TickerPrice{}, fmt.Errorf("mexc provider failed to get ticker price for %s", key) + } + + return ticker.toTickerPrice() +} + +func (p *MexcProvider) getCandlePrices(key string) ([]types.CandlePrice, error) { + p.mtx.RLock() + defer p.mtx.RUnlock() + + candles, ok := p.candles[key] + if !ok { + return []types.CandlePrice{}, fmt.Errorf("failed to get candle prices for %s", key) + } + + candleList := []types.CandlePrice{} + for _, candle := range candles { + cp, err := candle.toCandlePrice() + if err != nil { + return []types.CandlePrice{}, err + } + candleList = append(candleList, cp) + } + return candleList, nil +} + +func (p *MexcProvider) messageReceived(messageType int, bz []byte) { + if messageType != websocket.TextMessage { + return + } + + var ( + tickerResp MexcTickerResult + tickerErr error + candleResp MexcCandle + candleErr error + ) + + tickerErr = json.Unmarshal(bz, &tickerResp) + for _, cp := range p.subscribedPairs { + mexcPair := currencyPairToMexcPair(cp) + if tickerResp.Symbol[mexcPair].LastPrice != 0 { + p.setTickerPair( + mexcPair, + tickerResp.Symbol[mexcPair], + ) + telemetry.IncrCounter( + 1, + "websocket", + "message", + "type", + "ticker", + "provider", + string(ProviderMexc), + ) + return + } + } + + candleErr = json.Unmarshal(bz, &candleResp) + if candleResp.Metadata.Close != 0 { + p.setCandlePair(candleResp) + telemetry.IncrCounter( + 1, + "websocket", + "message", + "type", + "candle", + "provider", + string(ProviderMexc), + ) + return + } + + if tickerErr != nil || candleErr != nil { + p.logger.Error(). + Int("length", len(bz)). + AnErr("ticker", tickerErr). + AnErr("candle", candleErr). + Msg("mexc: Error on receive message") + } +} + +func (p *MexcProvider) setTickerPair(symbol string, ticker MexcTickerData) { + p.mtx.Lock() + defer p.mtx.Unlock() + + p.tickers[symbol] = MexcTicker{ + Symbol: symbol, + LastPrice: strconv.FormatFloat(ticker.LastPrice, 'f', 5, 64), + Volume: strconv.FormatFloat(ticker.Volume, 'f', 5, 64), + } +} + +func (p *MexcProvider) setCandlePair(candle MexcCandle) { + p.mtx.Lock() + defer p.mtx.Unlock() + staleTime := PastUnixTime(providerCandlePeriod) + candleList := []MexcCandle{} + // convert candle timestamp s -> milli + candle.Metadata.TimeStamp = SecondsToMilli(candle.Metadata.TimeStamp) + candleList = append(candleList, candle) + + for _, c := range p.candles[candle.Symbol] { + if staleTime < c.Metadata.TimeStamp { + candleList = append(candleList, c) + } + } + + p.candles[candle.Symbol] = candleList +} + +func (ticker MexcTicker) toTickerPrice() (types.TickerPrice, error) { + return types.NewTickerPrice("mexc", ticker.Symbol, ticker.LastPrice, ticker.Volume) +} + +func (candle MexcCandle) toCandlePrice() (types.CandlePrice, error) { + return types.NewCandlePrice( + "mexc", + candle.Symbol, + strconv.FormatFloat(candle.Metadata.Close, 'f', 5, 64), + strconv.FormatFloat(candle.Metadata.Volume, 'f', 5, 64), + candle.Metadata.TimeStamp, + ) +} + +func (p *MexcProvider) handleWebSocketMsgs(ctx context.Context) { + reconnectTicker := time.NewTicker(defaultMaxConnectionTime) + defer reconnectTicker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-time.After(defaultReadNewWSMessage): + messageType, bz, err := p.wsClient.ReadMessage() + if err != nil { + // if some error occurs continue to try to read the next message. + p.logger.Err(err).Msg("mexc: could not read message") + continue + } + + if len(bz) == 0 { + continue + } + + p.messageReceived(messageType, bz) + + case <-reconnectTicker.C: + if err := p.disconnect(); err != nil { + p.logger.Err(err).Msg("error closing websocket") + } + if err := p.reconnect(); err != nil { + p.logger.Err(err).Msg("mexc: error reconnecting") + p.keepReconnecting() + } + } + } +} + +func (p *MexcProvider) disconnect() error { + err := p.wsClient.Close() + if err != nil { + return types.ErrProviderConnection.Wrapf("error closing Mecx websocket %v", err) + } + return nil +} + +// reconnect closes the last WS connection then create a new one and subscribe to +// all subscribed pairs in the ticker and candle pairs. If no ping is received +// within 1 minute, the connection will be disconnected. It is recommended to +// send a ping for 10-20 seconds +func (p *MexcProvider) reconnect() error { + p.logger.Debug().Msg("mexc: reconnecting websocket") + wsConn, resp, err := websocket.DefaultDialer.Dial(p.wsURL.String(), nil) + defer resp.Body.Close() + if err != nil { + return fmt.Errorf("mexc: error reconnect to mexc websocket: %w", err) + } + p.wsClient = wsConn + + currencyPairs := p.subscribedPairsToSlice() + + telemetry.IncrCounter( + 1, + "websocket", + "reconnect", + "provider", + string(ProviderMexc), + ) + return p.subscribeChannels(currencyPairs...) +} + +// keepReconnecting keeps trying to reconnect if an error occurs in reconnect. +func (p *MexcProvider) keepReconnecting() { + reconnectTicker := time.NewTicker(defaultReconnectTime) + defer reconnectTicker.Stop() + connectionTries := 1 + + for time := range reconnectTicker.C { + if err := p.disconnect(); err != nil { + p.logger.Err(err).Msg("error closing websocket") + } + if err := p.reconnect(); err != nil { + p.logger.Err(err).Msgf("mexc: attempted to reconnect %d times at %s", connectionTries, time.String()) + connectionTries++ + continue + } + + if connectionTries > maxReconnectionTries { + p.logger.Warn().Msgf("mexc: failed to reconnect %d times", connectionTries) + } + return + } +} + +// setSubscribedPairs sets N currency pairs to the map of subscribed pairs. +func (p *MexcProvider) setSubscribedPairs(cps ...types.CurrencyPair) { + p.mtx.Lock() + defer p.mtx.Unlock() + + for _, cp := range cps { + p.subscribedPairs[cp.String()] = cp + } +} + +// subscribePairs write the subscription msg to the provider. +func (p *MexcProvider) subscribePairs(pairs ...string) error { + for _, cp := range pairs { + subsMsg := newMexcCandleSubscriptionMsg(cp) + err := p.wsClient.WriteJSON(subsMsg) + if err != nil { + return err + } + } + subsMsg := newMexcTickerSubscriptionMsg() + return p.wsClient.WriteJSON(subsMsg) +} + +// GetAvailablePairs returns all pairs to which the provider can subscribe. +// ex.: map["ATOMUSDT" => {}, "UMEEUSDC" => {}]. +func (p *MexcProvider) GetAvailablePairs() (map[string]struct{}, error) { + resp, err := http.Get(p.endpoints.Rest + mexcRestPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var pairsSummary []MexcPairSummary + if err := json.NewDecoder(resp.Body).Decode(&pairsSummary); err != nil { + return nil, err + } + + availablePairs := make(map[string]struct{}, len(pairsSummary)) + for _, pairName := range pairsSummary { + availablePairs[strings.ToUpper(pairName.Symbol)] = struct{}{} + } + + return availablePairs, nil +} + +// currencyPairToMexcPair receives a currency pair and return mexc +// ticker symbol atomusdt@ticker. +func currencyPairToMexcPair(cp types.CurrencyPair) string { + return strings.ToUpper(cp.Base + "_" + cp.Quote) +} + +// newMexcCandleSubscriptionMsg returns a new candle subscription Msg. +func newMexcCandleSubscriptionMsg(param string) MexcCandleSubscriptionMsg { + return MexcCandleSubscriptionMsg{ + OP: "sub.kline", + Symbol: param, + Interval: "Min1", + } +} + +// newMexcTickerSubscriptionMsg returns a new ticker subscription Msg. +func newMexcTickerSubscriptionMsg() MexcTickerSubscriptionMsg { + return MexcTickerSubscriptionMsg{ + OP: "sub.overview", + } +} diff --git a/price-feeder/oracle/provider/mexc_test.go b/price-feeder/oracle/provider/mexc_test.go new file mode 100644 index 0000000000..31f6d5681f --- /dev/null +++ b/price-feeder/oracle/provider/mexc_test.go @@ -0,0 +1,100 @@ +package provider + +import ( + "context" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "github.com/umee-network/umee/price-feeder/oracle/types" +) + +func TestMexcProvider_GetTickerPrices(t *testing.T) { + p, err := NewMexcProvider( + context.TODO(), + zerolog.Nop(), + Endpoint{}, + types.CurrencyPair{Base: "ATOM", Quote: "USDT"}, + ) + require.NoError(t, err) + + t.Run("valid_request_single_ticker", func(t *testing.T) { + lastPrice := "34.69000000" + volume := "2396974.02000000" + + tickerMap := map[string]MexcTicker{} + tickerMap["ATOMUSDT"] = MexcTicker{ + Symbol: "ATOMUSDT", + LastPrice: lastPrice, + Volume: volume, + } + + p.tickers = tickerMap + + prices, err := p.GetTickerPrices(types.CurrencyPair{Base: "ATOM", Quote: "USDT"}) + require.NoError(t, err) + require.Len(t, prices, 1) + require.Equal(t, sdk.MustNewDecFromStr(lastPrice), prices["ATOMUSDT"].Price) + require.Equal(t, sdk.MustNewDecFromStr(volume), prices["ATOMUSDT"].Volume) + }) + + t.Run("valid_request_multi_ticker", func(t *testing.T) { + lastPriceAtom := "34.69000000" + lastPriceLuna := "41.35000000" + volume := "2396974.02000000" + + tickerMap := map[string]MexcTicker{} + tickerMap["ATOMUSDT"] = MexcTicker{ + Symbol: "ATOMUSDT", + LastPrice: lastPriceAtom, + Volume: volume, + } + + tickerMap["LUNAUSDT"] = MexcTicker{ + Symbol: "LUNAUSDT", + LastPrice: lastPriceLuna, + Volume: volume, + } + + p.tickers = tickerMap + prices, err := p.GetTickerPrices( + types.CurrencyPair{Base: "ATOM", Quote: "USDT"}, + types.CurrencyPair{Base: "LUNA", Quote: "USDT"}, + ) + require.NoError(t, err) + require.Len(t, prices, 2) + require.Equal(t, sdk.MustNewDecFromStr(lastPriceAtom), prices["ATOMUSDT"].Price) + require.Equal(t, sdk.MustNewDecFromStr(volume), prices["ATOMUSDT"].Volume) + require.Equal(t, sdk.MustNewDecFromStr(lastPriceLuna), prices["LUNAUSDT"].Price) + require.Equal(t, sdk.MustNewDecFromStr(volume), prices["LUNAUSDT"].Volume) + }) + + t.Run("invalid_request_invalid_ticker", func(t *testing.T) { + prices, err := p.GetTickerPrices(types.CurrencyPair{Base: "FOO", Quote: "BAR"}) + require.Error(t, err) + require.Equal(t, "mexc provider failed to get ticker price for FOOBAR", err.Error()) + require.Nil(t, prices) + }) +} + +func TestMexcProvider_SubscribeCurrencyPairs(t *testing.T) { + p, err := NewMexcProvider( + context.TODO(), + zerolog.Nop(), + Endpoint{}, + types.CurrencyPair{Base: "ATOM", Quote: "USDT"}, + ) + require.NoError(t, err) + + t.Run("invalid_subscribe_channels_empty", func(t *testing.T) { + err = p.SubscribeCurrencyPairs([]types.CurrencyPair{}...) + require.ErrorContains(t, err, "currency pairs is empty") + }) +} + +func TestMexcCurrencyPairToMexcPair(t *testing.T) { + cp := types.CurrencyPair{Base: "ATOM", Quote: "USDT"} + MexcSymbol := currencyPairToMexcPair(cp) + require.Equal(t, MexcSymbol, "ATOM_USDT") +} diff --git a/price-feeder/oracle/provider/provider.go b/price-feeder/oracle/provider/provider.go index 94d262eacd..f0f9613631 100644 --- a/price-feeder/oracle/provider/provider.go +++ b/price-feeder/oracle/provider/provider.go @@ -25,6 +25,7 @@ const ( ProviderCoinbase Name = "coinbase" ProviderFTX Name = "ftx" ProviderBitget Name = "bitget" + ProviderMexc Name = "mexc" ProviderMock Name = "mock" ) From 353fe6dd6454d8d42dfff76ff20a138bd555caf6 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:21:40 -0700 Subject: [PATCH 02/10] update tests :^) --- price-feeder/oracle/provider/mexc_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/price-feeder/oracle/provider/mexc_test.go b/price-feeder/oracle/provider/mexc_test.go index 31f6d5681f..a6e2693529 100644 --- a/price-feeder/oracle/provider/mexc_test.go +++ b/price-feeder/oracle/provider/mexc_test.go @@ -24,8 +24,8 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { volume := "2396974.02000000" tickerMap := map[string]MexcTicker{} - tickerMap["ATOMUSDT"] = MexcTicker{ - Symbol: "ATOMUSDT", + tickerMap["ATOM_USDT"] = MexcTicker{ + Symbol: "ATOM_USDT", LastPrice: lastPrice, Volume: volume, } @@ -45,14 +45,14 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { volume := "2396974.02000000" tickerMap := map[string]MexcTicker{} - tickerMap["ATOMUSDT"] = MexcTicker{ - Symbol: "ATOMUSDT", + tickerMap["ATOM_USDT"] = MexcTicker{ + Symbol: "ATOM_USDT", LastPrice: lastPriceAtom, Volume: volume, } - tickerMap["LUNAUSDT"] = MexcTicker{ - Symbol: "LUNAUSDT", + tickerMap["LUNA_USDT"] = MexcTicker{ + Symbol: "LUNA_USDT", LastPrice: lastPriceLuna, Volume: volume, } @@ -73,7 +73,7 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { t.Run("invalid_request_invalid_ticker", func(t *testing.T) { prices, err := p.GetTickerPrices(types.CurrencyPair{Base: "FOO", Quote: "BAR"}) require.Error(t, err) - require.Equal(t, "mexc provider failed to get ticker price for FOOBAR", err.Error()) + require.Equal(t, "mexc provider failed to get ticker price for FOO_BAR", err.Error()) require.Nil(t, prices) }) } From d52a93d49a8849412dd02d608a228c693fa8eeb2 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:27:05 -0700 Subject: [PATCH 03/10] stop storing mexc tickers & candles as response objects --- price-feeder/oracle/provider/mexc.go | 116 ++++++++++------------ price-feeder/oracle/provider/mexc_test.go | 47 ++++----- 2 files changed, 72 insertions(+), 91 deletions(-) diff --git a/price-feeder/oracle/provider/mexc.go b/price-feeder/oracle/provider/mexc.go index a060d5df46..70d64d7a52 100644 --- a/price-feeder/oracle/provider/mexc.go +++ b/price-feeder/oracle/provider/mexc.go @@ -12,6 +12,7 @@ import ( "time" "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/umee-network/umee/price-feeder/oracle/types" @@ -39,48 +40,31 @@ type ( logger zerolog.Logger mtx sync.RWMutex endpoints Endpoint - tickers map[string]MexcTicker // Symbol => MexcTicker - candles map[string][]MexcCandle // Symbol => MexcCandle - subscribedPairs map[string]types.CurrencyPair // Symbol => types.CurrencyPair + tickers map[string]types.TickerPrice // Symbol => TickerPrice + candles map[string][]types.CandlePrice // Symbol => CandlePrice + subscribedPairs map[string]types.CurrencyPair // Symbol => types.CurrencyPair } - // MexcTicker ticker price response. https://pkg.go.dev/encoding/json#Unmarshal - // Unmarshal matches incoming object keys to the keys used by Marshal (either the - // struct field name or its tag), preferring an exact match but also accepting a - // case-insensitive match. C field which is Statistics close time is not used, but - // it avoids to implement specific UnmarshalJSON. - - MexcTicker struct { - Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT - LastPrice string `json:"p"` // Last price ex.: 0.0025 - Volume string `json:"v"` // Total traded base asset volume ex.: 1000 - C uint64 `json:"C"` // Statistics close time + // MexcTickerResponse is the ticker price response object. + MexcTickerResponse struct { + Symbol map[string]MexcTickerData `json:"data"` // e.x. ATOM_USDT } - MexcTickerData struct { LastPrice float64 `json:"p"` // Last price ex.: 0.0025 Volume float64 `json:"v"` // Total traded base asset volume ex.: 1000 } - MexcTickerResult struct { - Channel string `json:"channel"` // expect "push.overview" - Symbol map[string]MexcTickerData `json:"data"` // this key is the Symbol ex.: ATOM_USDT + // MexcCandle is the candle websocket response object. + MexcCandleResponse struct { + Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT + Metadata MexcCandleMetadata `json:"data"` // Metadata for candle } - - // MexcCandleMetadata candle metadata used to compute tvwap price. MexcCandleMetadata struct { Close float64 `json:"c"` // Price at close TimeStamp int64 `json:"t"` // Close time in unix epoch ex.: 1645756200000 Volume float64 `json:"v"` // Volume during period } - // MexcCandle candle Mexc websocket channel "kline_1m" response. - MexcCandle struct { - // Channel string `json:"channel"` // expect "push.kline" - Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT - Metadata MexcCandleMetadata `json:"data"` // Metadata for candle - } - // MexcSubscribeMsg Msg to subscribe all the tickers channels. MexcCandleSubscriptionMsg struct { OP string `json:"op"` // kline @@ -131,8 +115,8 @@ func NewMexcProvider( wsClient: wsConn, logger: logger.With().Str("provider", "mexc").Logger(), endpoints: endpoints, - tickers: map[string]MexcTicker{}, - candles: map[string][]MexcCandle{}, + tickers: map[string]types.TickerPrice{}, + candles: map[string][]types.CandlePrice{}, subscribedPairs: map[string]types.CurrencyPair{}, } @@ -239,7 +223,7 @@ func (p *MexcProvider) getTickerPrice(key string) (types.TickerPrice, error) { return types.TickerPrice{}, fmt.Errorf("mexc provider failed to get ticker price for %s", key) } - return ticker.toTickerPrice() + return ticker, nil } func (p *MexcProvider) getCandlePrices(key string) ([]types.CandlePrice, error) { @@ -251,15 +235,7 @@ func (p *MexcProvider) getCandlePrices(key string) ([]types.CandlePrice, error) return []types.CandlePrice{}, fmt.Errorf("failed to get candle prices for %s", key) } - candleList := []types.CandlePrice{} - for _, candle := range candles { - cp, err := candle.toCandlePrice() - if err != nil { - return []types.CandlePrice{}, err - } - candleList = append(candleList, cp) - } - return candleList, nil + return candles, nil } func (p *MexcProvider) messageReceived(messageType int, bz []byte) { @@ -268,9 +244,9 @@ func (p *MexcProvider) messageReceived(messageType int, bz []byte) { } var ( - tickerResp MexcTickerResult + tickerResp MexcTickerResponse tickerErr error - candleResp MexcCandle + candleResp MexcCandleResponse candleErr error ) @@ -323,43 +299,51 @@ func (p *MexcProvider) setTickerPair(symbol string, ticker MexcTickerData) { p.mtx.Lock() defer p.mtx.Unlock() - p.tickers[symbol] = MexcTicker{ - Symbol: symbol, - LastPrice: strconv.FormatFloat(ticker.LastPrice, 'f', 5, 64), - Volume: strconv.FormatFloat(ticker.Volume, 'f', 5, 64), + price, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.LastPrice, 'f', 5, 64)) + if err != nil { + p.logger.Warn().Err(err).Msg("mexc: failed to parse ticker price") + } + volume, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.Volume, 'f', 5, 64)) + if err != nil { + p.logger.Warn().Err(err).Msg("mexc: failed to parse ticker volume") + } + + p.tickers[symbol] = types.TickerPrice{ + Price: price, + Volume: volume, } } -func (p *MexcProvider) setCandlePair(candle MexcCandle) { +func (p *MexcProvider) setCandlePair(candleResp MexcCandleResponse) { p.mtx.Lock() defer p.mtx.Unlock() + + close, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Close, 'f', 5, 64)) + if err != nil { + p.logger.Warn().Err(err).Msg("mexc: failed to parse candle close") + } + volume, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Volume, 'f', 5, 64)) + if err != nil { + p.logger.Warn().Err(err).Msg("mexc: failed to parse candle volume") + } + candle := types.CandlePrice{ + Price: close, + Volume: volume, + // convert seconds -> milli + TimeStamp: SecondsToMilli(candleResp.Metadata.TimeStamp), + } + staleTime := PastUnixTime(providerCandlePeriod) - candleList := []MexcCandle{} - // convert candle timestamp s -> milli - candle.Metadata.TimeStamp = SecondsToMilli(candle.Metadata.TimeStamp) + candleList := []types.CandlePrice{} candleList = append(candleList, candle) - for _, c := range p.candles[candle.Symbol] { - if staleTime < c.Metadata.TimeStamp { + for _, c := range p.candles[candleResp.Symbol] { + if staleTime < c.TimeStamp { candleList = append(candleList, c) } } - p.candles[candle.Symbol] = candleList -} - -func (ticker MexcTicker) toTickerPrice() (types.TickerPrice, error) { - return types.NewTickerPrice("mexc", ticker.Symbol, ticker.LastPrice, ticker.Volume) -} - -func (candle MexcCandle) toCandlePrice() (types.CandlePrice, error) { - return types.NewCandlePrice( - "mexc", - candle.Symbol, - strconv.FormatFloat(candle.Metadata.Close, 'f', 5, 64), - strconv.FormatFloat(candle.Metadata.Volume, 'f', 5, 64), - candle.Metadata.TimeStamp, - ) + p.candles[candleResp.Symbol] = candleList } func (p *MexcProvider) handleWebSocketMsgs(ctx context.Context) { diff --git a/price-feeder/oracle/provider/mexc_test.go b/price-feeder/oracle/provider/mexc_test.go index a6e2693529..070f44d216 100644 --- a/price-feeder/oracle/provider/mexc_test.go +++ b/price-feeder/oracle/provider/mexc_test.go @@ -20,14 +20,13 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { require.NoError(t, err) t.Run("valid_request_single_ticker", func(t *testing.T) { - lastPrice := "34.69000000" - volume := "2396974.02000000" + lastPrice := sdk.MustNewDecFromStr("34.69000000") + volume := sdk.MustNewDecFromStr("2396974.02000000") - tickerMap := map[string]MexcTicker{} - tickerMap["ATOM_USDT"] = MexcTicker{ - Symbol: "ATOM_USDT", - LastPrice: lastPrice, - Volume: volume, + tickerMap := map[string]types.TickerPrice{} + tickerMap["ATOM_USDT"] = types.TickerPrice{ + Price: lastPrice, + Volume: volume, } p.tickers = tickerMap @@ -35,26 +34,24 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { prices, err := p.GetTickerPrices(types.CurrencyPair{Base: "ATOM", Quote: "USDT"}) require.NoError(t, err) require.Len(t, prices, 1) - require.Equal(t, sdk.MustNewDecFromStr(lastPrice), prices["ATOMUSDT"].Price) - require.Equal(t, sdk.MustNewDecFromStr(volume), prices["ATOMUSDT"].Volume) + require.Equal(t, lastPrice, prices["ATOMUSDT"].Price) + require.Equal(t, volume, prices["ATOMUSDT"].Volume) }) t.Run("valid_request_multi_ticker", func(t *testing.T) { - lastPriceAtom := "34.69000000" - lastPriceLuna := "41.35000000" - volume := "2396974.02000000" + lastPriceAtom := sdk.MustNewDecFromStr("34.69000000") + lastPriceLuna := sdk.MustNewDecFromStr("41.35000000") + volume := sdk.MustNewDecFromStr("2396974.02000000") - tickerMap := map[string]MexcTicker{} - tickerMap["ATOM_USDT"] = MexcTicker{ - Symbol: "ATOM_USDT", - LastPrice: lastPriceAtom, - Volume: volume, + tickerMap := map[string]types.TickerPrice{} + tickerMap["ATOM_USDT"] = types.TickerPrice{ + Price: lastPriceAtom, + Volume: volume, } - tickerMap["LUNA_USDT"] = MexcTicker{ - Symbol: "LUNA_USDT", - LastPrice: lastPriceLuna, - Volume: volume, + tickerMap["LUNA_USDT"] = types.TickerPrice{ + Price: lastPriceLuna, + Volume: volume, } p.tickers = tickerMap @@ -64,10 +61,10 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { ) require.NoError(t, err) require.Len(t, prices, 2) - require.Equal(t, sdk.MustNewDecFromStr(lastPriceAtom), prices["ATOMUSDT"].Price) - require.Equal(t, sdk.MustNewDecFromStr(volume), prices["ATOMUSDT"].Volume) - require.Equal(t, sdk.MustNewDecFromStr(lastPriceLuna), prices["LUNAUSDT"].Price) - require.Equal(t, sdk.MustNewDecFromStr(volume), prices["LUNAUSDT"].Volume) + require.Equal(t, lastPriceAtom, prices["ATOMUSDT"].Price) + require.Equal(t, volume, prices["ATOMUSDT"].Volume) + require.Equal(t, lastPriceLuna, prices["LUNAUSDT"].Price) + require.Equal(t, volume, prices["LUNAUSDT"].Volume) }) t.Run("invalid_request_invalid_ticker", func(t *testing.T) { From 7054d83c9bb806e4b8f635324676bf9d16c44843 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:24:50 -0700 Subject: [PATCH 04/10] PR review --- price-feeder/CHANGELOG.md | 1 + price-feeder/go.mod | 2 +- price-feeder/oracle/provider/mexc.go | 105 +++++++++------------------ price-feeder/oracle/types/errors.go | 2 + 4 files changed, 38 insertions(+), 72 deletions(-) diff --git a/price-feeder/CHANGELOG.md b/price-feeder/CHANGELOG.md index cc5382080b..5471a14ea0 100644 --- a/price-feeder/CHANGELOG.md +++ b/price-feeder/CHANGELOG.md @@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features - [1328](https://github.com/umee-network/umee/pull/1328) Add bitget provider. +- [1339](https://github.com/umee-network/umee/pull/1339) Add mexc provider. ### Bugs diff --git a/price-feeder/go.mod b/price-feeder/go.mod index bddde5a0cf..f4f8ced655 100644 --- a/price-feeder/go.mod +++ b/price-feeder/go.mod @@ -3,6 +3,7 @@ module github.com/umee-network/umee/price-feeder go 1.19 require ( + github.com/armon/go-metrics v0.4.0 github.com/cosmos/cosmos-sdk v0.46.1 github.com/go-playground/validator/v10 v10.11.0 github.com/golangci/golangci-lint v1.49.0 @@ -45,7 +46,6 @@ require ( github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect - github.com/armon/go-metrics v0.4.0 // indirect github.com/ashanbrown/forbidigo v1.3.0 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect github.com/aws/aws-sdk-go v1.40.45 // indirect diff --git a/price-feeder/oracle/provider/mexc.go b/price-feeder/oracle/provider/mexc.go index 70d64d7a52..ef0afea50f 100644 --- a/price-feeder/oracle/provider/mexc.go +++ b/price-feeder/oracle/provider/mexc.go @@ -47,33 +47,33 @@ type ( // MexcTickerResponse is the ticker price response object. MexcTickerResponse struct { - Symbol map[string]MexcTickerData `json:"data"` // e.x. ATOM_USDT + Symbol map[string]MexcTicker `json:"data"` // e.x. ATOM_USDT } - MexcTickerData struct { + MexcTicker struct { LastPrice float64 `json:"p"` // Last price ex.: 0.0025 Volume float64 `json:"v"` // Total traded base asset volume ex.: 1000 } // MexcCandle is the candle websocket response object. MexcCandleResponse struct { - Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT - Metadata MexcCandleMetadata `json:"data"` // Metadata for candle + Symbol string `json:"symbol"` // Symbol ex.: ATOM_USDT + Metadata MexcCandle `json:"data"` // Metadata for candle } - MexcCandleMetadata struct { + MexcCandle struct { Close float64 `json:"c"` // Price at close TimeStamp int64 `json:"t"` // Close time in unix epoch ex.: 1645756200000 Volume float64 `json:"v"` // Volume during period } - // MexcSubscribeMsg Msg to subscribe all the tickers channels. - MexcCandleSubscriptionMsg struct { + // MexcCandleSubscription Msg to subscribe all the candle channels. + MexcCandleSubscription struct { OP string `json:"op"` // kline Symbol string `json:"symbol"` // streams to subscribe ex.: atom_usdt Interval string `json:"interval"` // Min1、Min5、Min15、Min30 } - // MexcSubscribeMsg Msg to subscribe all the tickers channels. - MexcTickerSubscriptionMsg struct { + // MexcTickerSubscription Msg to subscribe all the ticker channels. + MexcTickerSubscription struct { OP string `json:"op"` // kline } @@ -220,7 +220,11 @@ func (p *MexcProvider) getTickerPrice(key string) (types.TickerPrice, error) { ticker, ok := p.tickers[key] if !ok { - return types.TickerPrice{}, fmt.Errorf("mexc provider failed to get ticker price for %s", key) + return types.TickerPrice{}, fmt.Errorf( + types.ErrTickerNotFound.Error(), + ProviderMexc, + key, + ) } return ticker, nil @@ -232,7 +236,11 @@ func (p *MexcProvider) getCandlePrices(key string) ([]types.CandlePrice, error) candles, ok := p.candles[key] if !ok { - return []types.CandlePrice{}, fmt.Errorf("failed to get candle prices for %s", key) + return []types.CandlePrice{}, fmt.Errorf( + types.ErrCandleNotFound.Error(), + ProviderMexc, + key, + ) } return candles, nil @@ -258,15 +266,7 @@ func (p *MexcProvider) messageReceived(messageType int, bz []byte) { mexcPair, tickerResp.Symbol[mexcPair], ) - telemetry.IncrCounter( - 1, - "websocket", - "message", - "type", - "ticker", - "provider", - string(ProviderMexc), - ) + telemetryWebsocketReconnect(ProviderMexc) return } } @@ -295,7 +295,7 @@ func (p *MexcProvider) messageReceived(messageType int, bz []byte) { } } -func (p *MexcProvider) setTickerPair(symbol string, ticker MexcTickerData) { +func (p *MexcProvider) setTickerPair(symbol string, ticker MexcTicker) { p.mtx.Lock() defer p.mtx.Unlock() @@ -369,71 +369,34 @@ func (p *MexcProvider) handleWebSocketMsgs(ctx context.Context) { p.messageReceived(messageType, bz) case <-reconnectTicker.C: - if err := p.disconnect(); err != nil { - p.logger.Err(err).Msg("error closing websocket") - } if err := p.reconnect(); err != nil { - p.logger.Err(err).Msg("mexc: error reconnecting") - p.keepReconnecting() + p.logger.Err(err).Msg("error reconnecting") } } } } -func (p *MexcProvider) disconnect() error { +// reconnect closes the last WS connection then create a new one and subscribes to +// all subscribed pairs in the ticker and candle pairs. If no ping is received +// within 1 minute, the connection will be disconnected. It is recommended to +// send a ping for 10-20 seconds +func (p *MexcProvider) reconnect() error { err := p.wsClient.Close() if err != nil { return types.ErrProviderConnection.Wrapf("error closing Mecx websocket %v", err) } - return nil -} -// reconnect closes the last WS connection then create a new one and subscribe to -// all subscribed pairs in the ticker and candle pairs. If no ping is received -// within 1 minute, the connection will be disconnected. It is recommended to -// send a ping for 10-20 seconds -func (p *MexcProvider) reconnect() error { p.logger.Debug().Msg("mexc: reconnecting websocket") + wsConn, resp, err := websocket.DefaultDialer.Dial(p.wsURL.String(), nil) defer resp.Body.Close() if err != nil { return fmt.Errorf("mexc: error reconnect to mexc websocket: %w", err) } p.wsClient = wsConn + telemetryWebsocketReconnect(ProviderMexc) - currencyPairs := p.subscribedPairsToSlice() - - telemetry.IncrCounter( - 1, - "websocket", - "reconnect", - "provider", - string(ProviderMexc), - ) - return p.subscribeChannels(currencyPairs...) -} - -// keepReconnecting keeps trying to reconnect if an error occurs in reconnect. -func (p *MexcProvider) keepReconnecting() { - reconnectTicker := time.NewTicker(defaultReconnectTime) - defer reconnectTicker.Stop() - connectionTries := 1 - - for time := range reconnectTicker.C { - if err := p.disconnect(); err != nil { - p.logger.Err(err).Msg("error closing websocket") - } - if err := p.reconnect(); err != nil { - p.logger.Err(err).Msgf("mexc: attempted to reconnect %d times at %s", connectionTries, time.String()) - connectionTries++ - continue - } - - if connectionTries > maxReconnectionTries { - p.logger.Warn().Msgf("mexc: failed to reconnect %d times", connectionTries) - } - return - } + return p.subscribeChannels(p.subscribedPairsToSlice()...) } // setSubscribedPairs sets N currency pairs to the map of subscribed pairs. @@ -488,8 +451,8 @@ func currencyPairToMexcPair(cp types.CurrencyPair) string { } // newMexcCandleSubscriptionMsg returns a new candle subscription Msg. -func newMexcCandleSubscriptionMsg(param string) MexcCandleSubscriptionMsg { - return MexcCandleSubscriptionMsg{ +func newMexcCandleSubscriptionMsg(param string) MexcCandleSubscription { + return MexcCandleSubscription{ OP: "sub.kline", Symbol: param, Interval: "Min1", @@ -497,8 +460,8 @@ func newMexcCandleSubscriptionMsg(param string) MexcCandleSubscriptionMsg { } // newMexcTickerSubscriptionMsg returns a new ticker subscription Msg. -func newMexcTickerSubscriptionMsg() MexcTickerSubscriptionMsg { - return MexcTickerSubscriptionMsg{ +func newMexcTickerSubscriptionMsg() MexcTickerSubscription { + return MexcTickerSubscription{ OP: "sub.overview", } } diff --git a/price-feeder/oracle/types/errors.go b/price-feeder/oracle/types/errors.go index 107996aae2..c3e3483566 100644 --- a/price-feeder/oracle/types/errors.go +++ b/price-feeder/oracle/types/errors.go @@ -10,4 +10,6 @@ const ModuleName = "price-feeder" var ( ErrProviderConnection = sdkerrors.Register(ModuleName, 1, "provider connection") ErrMissingExchangeRate = sdkerrors.Register(ModuleName, 2, "missing exchange rate for %s") + ErrTickerNotFound = sdkerrors.Register(ModuleName, 3, "%s failed to get ticker price for %s") + ErrCandleNotFound = sdkerrors.Register(ModuleName, 4, "%s failed to get candle price for %s") ) From 85f92f16252e4f13e8a3e81437b8f97df13597e2 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:29:23 -0700 Subject: [PATCH 05/10] some more fixes :^) --- price-feeder/oracle/provider/mexc.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/price-feeder/oracle/provider/mexc.go b/price-feeder/oracle/provider/mexc.go index ef0afea50f..ce239ef6f3 100644 --- a/price-feeder/oracle/provider/mexc.go +++ b/price-feeder/oracle/provider/mexc.go @@ -11,7 +11,6 @@ import ( "sync" "time" - "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/gorilla/websocket" "github.com/rs/zerolog" @@ -266,7 +265,7 @@ func (p *MexcProvider) messageReceived(messageType int, bz []byte) { mexcPair, tickerResp.Symbol[mexcPair], ) - telemetryWebsocketReconnect(ProviderMexc) + telemetryWebsocketMessage(ProviderMexc, MessageTypeTicker) return } } @@ -274,15 +273,7 @@ func (p *MexcProvider) messageReceived(messageType int, bz []byte) { candleErr = json.Unmarshal(bz, &candleResp) if candleResp.Metadata.Close != 0 { p.setCandlePair(candleResp) - telemetry.IncrCounter( - 1, - "websocket", - "message", - "type", - "candle", - "provider", - string(ProviderMexc), - ) + telemetryWebsocketMessage(ProviderMexc, MessageTypeCandle) return } From 70754d991a91d6eca662de536d03c08b4b206056 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:32:54 -0700 Subject: [PATCH 06/10] fix float formats --- price-feeder/oracle/provider/mexc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/price-feeder/oracle/provider/mexc.go b/price-feeder/oracle/provider/mexc.go index ce239ef6f3..e5f9d5a987 100644 --- a/price-feeder/oracle/provider/mexc.go +++ b/price-feeder/oracle/provider/mexc.go @@ -290,11 +290,11 @@ func (p *MexcProvider) setTickerPair(symbol string, ticker MexcTicker) { p.mtx.Lock() defer p.mtx.Unlock() - price, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.LastPrice, 'f', 5, 64)) + price, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.LastPrice, 'f', -1, 64)) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse ticker price") } - volume, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.Volume, 'f', 5, 64)) + volume, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.Volume, 'f', -1, 64)) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse ticker volume") } @@ -309,11 +309,11 @@ func (p *MexcProvider) setCandlePair(candleResp MexcCandleResponse) { p.mtx.Lock() defer p.mtx.Unlock() - close, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Close, 'f', 5, 64)) + close, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Close, 'f', -1, 64)) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse candle close") } - volume, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Volume, 'f', 5, 64)) + volume, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Volume, 'f', -1, 64)) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse candle volume") } From 1345fab62c14c183553d1fe6bcb19cc66c581b58 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:34:56 -0700 Subject: [PATCH 07/10] fix tests! --- price-feeder/oracle/provider/mexc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price-feeder/oracle/provider/mexc_test.go b/price-feeder/oracle/provider/mexc_test.go index 070f44d216..49d3d9de6a 100644 --- a/price-feeder/oracle/provider/mexc_test.go +++ b/price-feeder/oracle/provider/mexc_test.go @@ -70,7 +70,7 @@ func TestMexcProvider_GetTickerPrices(t *testing.T) { t.Run("invalid_request_invalid_ticker", func(t *testing.T) { prices, err := p.GetTickerPrices(types.CurrencyPair{Base: "FOO", Quote: "BAR"}) require.Error(t, err) - require.Equal(t, "mexc provider failed to get ticker price for FOO_BAR", err.Error()) + require.Equal(t, "mexc failed to get ticker price for FOO_BAR", err.Error()) require.Nil(t, prices) }) } From 607877885ff5d12d0feb28ca7434c094e5f5a609 Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 14 Sep 2022 13:46:21 -0700 Subject: [PATCH 08/10] add NewDecFromFloat & MustNewDecFromFloat helper functions --- price-feeder/oracle/provider/mexc.go | 12 ++++++------ util/coin/dec.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 util/coin/dec.go diff --git a/price-feeder/oracle/provider/mexc.go b/price-feeder/oracle/provider/mexc.go index e5f9d5a987..64d2aac2ad 100644 --- a/price-feeder/oracle/provider/mexc.go +++ b/price-feeder/oracle/provider/mexc.go @@ -6,15 +6,15 @@ import ( "fmt" "net/http" "net/url" - "strconv" "strings" "sync" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/umee-network/umee/price-feeder/oracle/types" + + "github.com/umee-network/umee/v3/util/coin" ) const ( @@ -290,11 +290,11 @@ func (p *MexcProvider) setTickerPair(symbol string, ticker MexcTicker) { p.mtx.Lock() defer p.mtx.Unlock() - price, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.LastPrice, 'f', -1, 64)) + price, err := coin.NewDecFromFloat(ticker.LastPrice) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse ticker price") } - volume, err := sdk.NewDecFromStr(strconv.FormatFloat(ticker.Volume, 'f', -1, 64)) + volume, err := coin.NewDecFromFloat(ticker.Volume) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse ticker volume") } @@ -309,11 +309,11 @@ func (p *MexcProvider) setCandlePair(candleResp MexcCandleResponse) { p.mtx.Lock() defer p.mtx.Unlock() - close, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Close, 'f', -1, 64)) + close, err := coin.NewDecFromFloat(candleResp.Metadata.Close) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse candle close") } - volume, err := sdk.NewDecFromStr(strconv.FormatFloat(candleResp.Metadata.Volume, 'f', -1, 64)) + volume, err := coin.NewDecFromFloat(candleResp.Metadata.Volume) if err != nil { p.logger.Warn().Err(err).Msg("mexc: failed to parse candle volume") } diff --git a/util/coin/dec.go b/util/coin/dec.go new file mode 100644 index 0000000000..fb417b12ef --- /dev/null +++ b/util/coin/dec.go @@ -0,0 +1,15 @@ +package coin + +import ( + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewDecFromFloat(f float64) (sdk.Dec, error) { + return sdk.NewDecFromStr(strconv.FormatFloat(f, 'f', -1, 64)) +} + +func MustNewDecFromFloat(f float64) sdk.Dec { + return sdk.MustNewDecFromStr(strconv.FormatFloat(f, 'f', -1, 64)) +} From e70b8d26130273412a542da905541838a302e5db Mon Sep 17 00:00:00 2001 From: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:17:58 -0700 Subject: [PATCH 09/10] readme++ --- price-feeder/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/price-feeder/README.md b/price-feeder/README.md index f695976957..f966b1d439 100644 --- a/price-feeder/README.md +++ b/price-feeder/README.md @@ -33,6 +33,7 @@ The list of current supported providers: - [Gate](https://www.gate.io/) - [Huobi](https://www.huobi.com/en-us/) - [Kraken](https://www.kraken.com/en-us/) +- [Mexc](https://www.mexc.com/) - [Okx](https://www.okx.com/) - [Osmosis](https://app.osmosis.zone/) From 873f867ca272ecebc797a1101be14d0312f121d6 Mon Sep 17 00:00:00 2001 From: RafilxTenfen Date: Wed, 14 Sep 2022 22:05:36 -0300 Subject: [PATCH 10/10] fix broken link --- price-feeder/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/price-feeder/README.md b/price-feeder/README.md index f966b1d439..f77366d60c 100644 --- a/price-feeder/README.md +++ b/price-feeder/README.md @@ -19,7 +19,7 @@ The `price-feeder` tool is responsible for performing the following: Binance and Osmosis, based on operator configuration. These exchange rates are exposed via an API and are used to feed into the main oracle process. 2. Taking aggregated exchange rate price data and submitting those exchange rates - on-chain to Umee's `x/oracle` module following Umee's [Oracle](https://github.com/umee-network/umee/tree/main/x/oracle/spec) + on-chain to Umee's `x/oracle` module following Umee's [Oracle](https://github.com/umee-network/umee/tree/main/x/oracle#readme) specification.