Skip to content
This repository was archived by the owner on Mar 24, 2025. It is now read-only.

feat: providertest #791

Merged
merged 11 commits into from
Oct 9, 2024
Merged
Changes from 10 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 providers/apis/dydx/multi_market_map_fetcher.go
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ var (
}
)

// NewDYDXResearchMarketMapFetcher returns a MultiMarketMapFetcher composed of dydx mainnet + research
// DefaultDYDXResearchMarketMapFetcher returns a MultiMarketMapFetcher composed of dydx mainnet + research
// apiDataHandlers.
func DefaultDYDXResearchMarketMapFetcher(
rh apihandlers.RequestHandler,
67 changes: 67 additions & 0 deletions providers/providertest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Provider testing

## Example

The following example can be used as a base for testing providers.

```go
package providertest_test

import (
"context"
"testing"

"go.uber.org/zap"

"github.com/stretchr/testify/require"

connecttypes "github.com/skip-mev/connect/v2/pkg/types"
"github.com/skip-mev/connect/v2/providers/providertest"
mmtypes "github.com/skip-mev/connect/v2/x/marketmap/types"
)

var (
usdtusd = mmtypes.Market{
Ticker: mmtypes.Ticker{
CurrencyPair: connecttypes.CurrencyPair{
Base: "USDT",
Quote: "USD",
},
Decimals: 8,
MinProviderCount: 1,
Enabled: true,
},
ProviderConfigs: []mmtypes.ProviderConfig{
{
Name: "okx_ws",
OffChainTicker: "USDC-USDT",
Invert: true,
},
},
}

mm = mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusd.Ticker.String(): usdtusd,
},
}
)

func TestProvider(t *testing.T) {
// take in a market map and filter it to output N market maps with only a single provider
marketsPerProvider := providertest.FilterMarketMapToProviders(mm)

// run this check for each provider (here only okx_ws)
for provider, marketMap := range marketsPerProvider {
ctx := context.Background()
p, err := providertest.NewTestingOracle(ctx, provider)
require.NoError(t, err)

results, err := p.RunMarketMap(ctx, marketMap, providertest.DefaultProviderTestConfig())
require.NoError(t, err)

p.Logger.Info("results", zap.Any("results", results))
}
}

```
184 changes: 184 additions & 0 deletions providers/providertest/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package providertest

import (
"context"
"fmt"
"time"

"go.uber.org/zap"

"github.com/skip-mev/connect/v2/oracle"
oraclemetrics "github.com/skip-mev/connect/v2/oracle/metrics"
oracletypes "github.com/skip-mev/connect/v2/oracle/types"
"github.com/skip-mev/connect/v2/pkg/log"
oraclemath "github.com/skip-mev/connect/v2/pkg/math/oracle"
oraclefactory "github.com/skip-mev/connect/v2/providers/factories/oracle"
mmtypes "github.com/skip-mev/connect/v2/x/marketmap/types"
)

type TestingOracle struct {
Oracle *oracle.OracleImpl
Logger *zap.Logger
}

func (o *TestingOracle) Start(ctx context.Context) error {
return o.Oracle.Start(ctx)

Check warning on line 25 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L24-L25

Added lines #L24 - L25 were not covered by tests
}

func (o *TestingOracle) Stop() {
o.Oracle.Stop()

Check warning on line 29 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L28-L29

Added lines #L28 - L29 were not covered by tests
}

func (o *TestingOracle) GetPrices() oracletypes.Prices {
return o.Oracle.GetPrices()

Check warning on line 33 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L32-L33

Added lines #L32 - L33 were not covered by tests
}

func (o *TestingOracle) UpdateMarketMap(mm mmtypes.MarketMap) error {
return o.Oracle.UpdateMarketMap(mm)

Check warning on line 37 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L36-L37

Added lines #L36 - L37 were not covered by tests
}

func NewTestingOracle(ctx context.Context, providerNames ...string) (TestingOracle, error) {
logCfg := log.NewDefaultConfig()
logCfg.StdOutLogLevel = "debug"
logCfg.FileOutLogLevel = "debug"
logCfg.LogSamplePeriod = 250 * time.Millisecond
logger := log.NewLogger(logCfg)

Check warning on line 45 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L40-L45

Added lines #L40 - L45 were not covered by tests

agg, err := oraclemath.NewIndexPriceAggregator(logger, mmtypes.MarketMap{}, oraclemetrics.NewNopMetrics())
if err != nil {
return TestingOracle{}, fmt.Errorf("failed to create oracle index price aggregator: %w", err)

Check warning on line 49 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L47-L49

Added lines #L47 - L49 were not covered by tests
}

cfg, err := OracleConfigForProvider(providerNames...)
if err != nil {
return TestingOracle{}, fmt.Errorf("failed to create oracle config: %w", err)

Check warning on line 54 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L52-L54

Added lines #L52 - L54 were not covered by tests
}

orc, err := oracle.New(
cfg,
agg,
oracle.WithLogger(logger),
oracle.WithPriceAPIQueryHandlerFactory(oraclefactory.APIQueryHandlerFactory),
oracle.WithPriceWebSocketQueryHandlerFactory(oraclefactory.WebSocketQueryHandlerFactory),
oracle.WithMarketMapperFactory(oraclefactory.MarketMapProviderFactory),
)
if err != nil {
return TestingOracle{}, fmt.Errorf("failed to create oracle: %w", err)

Check warning on line 66 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L57-L66

Added lines #L57 - L66 were not covered by tests
}

o := orc.(*oracle.OracleImpl)
err = o.Init(ctx)
if err != nil {
return TestingOracle{}, err

Check warning on line 72 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L69-L72

Added lines #L69 - L72 were not covered by tests
}

return TestingOracle{
Oracle: o,
Logger: logger,
}, nil

Check warning on line 78 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L75-L78

Added lines #L75 - L78 were not covered by tests
}

// Config is used to configure tests when calling RunMarket or RunMarketMap.
type Config struct {
// TestDuration is the total duration of testing time.
TestDuration time.Duration
// PolInterval is the interval at which the provider will be queried for prices.
PollInterval time.Duration
// BurnInInterval is the amount of time to allow the provider to run before querying it.
BurnInInterval time.Duration
}

func (c *Config) Validate() error {
if c.TestDuration == 0 {
return fmt.Errorf("test duration cannot be 0")

Check warning on line 93 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L91-L93

Added lines #L91 - L93 were not covered by tests
}

if c.PollInterval == 0 {
return fmt.Errorf("poll interval cannot be 0")

Check warning on line 97 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L96-L97

Added lines #L96 - L97 were not covered by tests
}

if c.TestDuration/c.PollInterval < 1 {
return fmt.Errorf("ratio of test duration to poll interval must be GTE 1")

Check warning on line 101 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L100-L101

Added lines #L100 - L101 were not covered by tests
}

return nil

Check warning on line 104 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L104

Added line #L104 was not covered by tests
}

// DefaultProviderTestConfig tests by:
// - allow the providers to run for 5 seconds
// - test for a total of 1 minute
// - poll each 5 seconds for prices.
func DefaultProviderTestConfig() Config {
return Config{
TestDuration: 1 * time.Minute,
PollInterval: 5 * time.Second,
BurnInInterval: 5 * time.Second,

Check warning on line 115 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L111-L115

Added lines #L111 - L115 were not covered by tests
}
}

// PriceResults is a type alias for an array of PriceResult.
type PriceResults []PriceResult

// PriceResult is a snapshot of Prices results at a given time point when testing.
type PriceResult struct {
Prices oracletypes.Prices
Time time.Time
}

func (o *TestingOracle) RunMarketMap(ctx context.Context, mm mmtypes.MarketMap, cfg Config) (PriceResults, error) {
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)

Check warning on line 130 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L128-L130

Added lines #L128 - L130 were not covered by tests
}

err := o.UpdateMarketMap(mm)
if err != nil {
return nil, fmt.Errorf("failed to update oracle market map: %w", err)

Check warning on line 135 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L133-L135

Added lines #L133 - L135 were not covered by tests
}

expectedNumPrices := len(mm.Markets)
if expectedNumPrices == 0 {
return nil, fmt.Errorf("cannot test with empty market map")

Check warning on line 140 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L138-L140

Added lines #L138 - L140 were not covered by tests
}

go o.Start(ctx)
time.Sleep(cfg.BurnInInterval)

Check warning on line 144 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L143-L144

Added lines #L143 - L144 were not covered by tests

priceResults := make(PriceResults, 0, cfg.TestDuration/cfg.PollInterval)

Check warning on line 146 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L146

Added line #L146 was not covered by tests

ticker := time.NewTicker(cfg.PollInterval)
defer ticker.Stop()

Check warning on line 149 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L148-L149

Added lines #L148 - L149 were not covered by tests

timer := time.NewTicker(cfg.TestDuration)
defer timer.Stop()

Check warning on line 152 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L151-L152

Added lines #L151 - L152 were not covered by tests

for {
select {
case <-ticker.C:
prices := o.GetPrices()
if len(prices) != expectedNumPrices {
return nil, fmt.Errorf("expected %d prices, got %d", expectedNumPrices, len(prices))

Check warning on line 159 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L154-L159

Added lines #L154 - L159 were not covered by tests
}
o.Logger.Info("provider prices", zap.Any("prices", prices))
priceResults = append(priceResults, PriceResult{
Prices: prices,
Time: time.Now(),
})

Check warning on line 165 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L161-L165

Added lines #L161 - L165 were not covered by tests

case <-timer.C:
o.Stop()

Check warning on line 168 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L167-L168

Added lines #L167 - L168 were not covered by tests

// cleanup
return priceResults, nil

Check warning on line 171 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L171

Added line #L171 was not covered by tests
}
}
}

func (o *TestingOracle) RunMarket(ctx context.Context, market mmtypes.Market, cfg Config) (PriceResults, error) {
mm := mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
market.Ticker.String(): market,
},

Check warning on line 180 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L176-L180

Added lines #L176 - L180 were not covered by tests
}

return o.RunMarketMap(ctx, mm, cfg)

Check warning on line 183 in providers/providertest/provider.go

Codecov / codecov/patch

providers/providertest/provider.go#L183

Added line #L183 was not covered by tests
}
75 changes: 75 additions & 0 deletions providers/providertest/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package providertest

import (
"fmt"

cmdconfig "github.com/skip-mev/connect/v2/cmd/connect/config"
"github.com/skip-mev/connect/v2/cmd/constants"
"github.com/skip-mev/connect/v2/oracle/config"
mmtypes "github.com/skip-mev/connect/v2/x/marketmap/types"
)

func FilterMarketMapToProviders(mm mmtypes.MarketMap) map[string]mmtypes.MarketMap {
m := make(map[string]mmtypes.MarketMap)

for _, market := range mm.Markets {
// check each provider config
for _, pc := range market.ProviderConfigs {
// create a market from the given provider config
isolatedMarket := mmtypes.Market{
Ticker: market.Ticker,
ProviderConfigs: []mmtypes.ProviderConfig{
pc,
},
}

// always enable and set minprovider count to 1 so that it can be run isolated
isolatedMarket.Ticker.Enabled = true
isolatedMarket.Ticker.MinProviderCount = 1

// init mm if necessary
if _, found := m[pc.Name]; !found {
m[pc.Name] = mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
isolatedMarket.Ticker.String(): isolatedMarket,
},
}
// otherwise insert
} else {
m[pc.Name].Markets[isolatedMarket.Ticker.String()] = isolatedMarket
}
}
}

return m
}

func OracleConfigForProvider(providerNames ...string) (config.OracleConfig, error) {
cfg := config.OracleConfig{
UpdateInterval: cmdconfig.DefaultUpdateInterval,
MaxPriceAge: cmdconfig.DefaultMaxPriceAge,
Metrics: config.MetricsConfig{
Enabled: false,
Telemetry: config.TelemetryConfig{
Disabled: true,
},
},
Providers: make(map[string]config.ProviderConfig),
Host: cmdconfig.DefaultHost,
Port: cmdconfig.DefaultPort,

Check warning on line 59 in providers/providertest/util.go

Codecov / codecov/patch

providers/providertest/util.go#L47-L59

Added lines #L47 - L59 were not covered by tests
}

for _, provider := range append(constants.Providers, constants.AlternativeMarketMapProviders...) {
for _, providerName := range providerNames {
if provider.Name == providerName {
cfg.Providers[provider.Name] = provider

Check warning on line 65 in providers/providertest/util.go

Codecov / codecov/patch

providers/providertest/util.go#L62-L65

Added lines #L62 - L65 were not covered by tests
}
}
}

if err := cfg.ValidateBasic(); err != nil {
return cfg, fmt.Errorf("default oracle config is invalid: %w", err)

Check warning on line 71 in providers/providertest/util.go

Codecov / codecov/patch

providers/providertest/util.go#L70-L71

Added lines #L70 - L71 were not covered by tests
}

return cfg, nil

Check warning on line 74 in providers/providertest/util.go

Codecov / codecov/patch

providers/providertest/util.go#L74

Added line #L74 was not covered by tests
}
272 changes: 272 additions & 0 deletions providers/providertest/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package providertest_test

import (
"testing"

"github.com/stretchr/testify/require"

connecttypes "github.com/skip-mev/connect/v2/pkg/types"
"github.com/skip-mev/connect/v2/providers/providertest"
mmtypes "github.com/skip-mev/connect/v2/x/marketmap/types"
)

var (
usdtusdTicker = mmtypes.Ticker{
CurrencyPair: connecttypes.CurrencyPair{
Base: "USDT",
Quote: "USD",
},
Decimals: 8,
MinProviderCount: 1,
Enabled: true,
}

usdtusdTickerDisabled = mmtypes.Ticker{
CurrencyPair: connecttypes.CurrencyPair{
Base: "USDT",
Quote: "USD",
},
Decimals: 8,
MinProviderCount: 1,
Enabled: false,
}

usdtusdTickerMinProvider = mmtypes.Ticker{
CurrencyPair: connecttypes.CurrencyPair{
Base: "USDT",
Quote: "USD",
},
Decimals: 8,
MinProviderCount: 10,
Enabled: true,
}

usdtusdProviderCfgOkx = mmtypes.ProviderConfig{
Name: "okx_ws",
OffChainTicker: "USDC-USDT",
Invert: true,
}

usdtusdProviderCfgBitstamp = mmtypes.ProviderConfig{
Name: "bitstamp_api",
OffChainTicker: "USDT-USD",
Invert: false,
}

usdtusdSingleProvider = mmtypes.Market{
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgOkx,
},
}

usdtusdMultiProvider = mmtypes.Market{
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgOkx,
usdtusdProviderCfgBitstamp,
},
}

btctusdTicker = mmtypes.Ticker{
CurrencyPair: connecttypes.CurrencyPair{
Base: "BTC",
Quote: "USD",
},
Decimals: 8,
MinProviderCount: 1,
Enabled: true,
}

btcusdProviderCfgOkx = mmtypes.ProviderConfig{
Name: "okx_ws",
OffChainTicker: "USDC-BTC",
Invert: true,
}

btcusdProviderCfgBitstamp = mmtypes.ProviderConfig{
Name: "bitstamp_api",
OffChainTicker: "BTC-USD",
Invert: false,
}

btcusdSingleProvider = mmtypes.Market{
Ticker: btctusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
btcusdProviderCfgOkx,
},
}

btcusdMultiProvider = mmtypes.Market{
Ticker: btctusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
btcusdProviderCfgOkx,
btcusdProviderCfgBitstamp,
},
}
)

func TestFilterMarketMapToProviders(t *testing.T) {
tests := []struct {
name string
input mmtypes.MarketMap
want map[string]mmtypes.MarketMap
}{
{
name: "empty",
input: mmtypes.MarketMap{},
want: make(map[string]mmtypes.MarketMap),
},
{
name: "single market with one provider",
input: mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdSingleProvider,
},
},
want: map[string]mmtypes.MarketMap{
"okx_ws": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdSingleProvider,
},
},
},
},
{
name: "enable disabled markets for testing",
input: mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusdTickerDisabled.String(): usdtusdSingleProvider,
},
},
want: map[string]mmtypes.MarketMap{
"okx_ws": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdSingleProvider,
},
},
},
},
{
name: "set min provider count to 1 for testing",
input: mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusdTickerMinProvider.String(): usdtusdSingleProvider,
},
},
want: map[string]mmtypes.MarketMap{
"okx_ws": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdSingleProvider,
},
},
},
},
{
name: "single market with multi provider",
input: mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdMultiProvider,
},
},
want: map[string]mmtypes.MarketMap{
"okx_ws": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): {
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgOkx,
},
},
},
},
"bitstamp_api": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): {
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgBitstamp,
},
},
},
},
},
},
{
name: "multi market with single provider",
input: mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdSingleProvider,
btctusdTicker.String(): btcusdSingleProvider,
},
},
want: map[string]mmtypes.MarketMap{
"okx_ws": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): {
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgOkx,
},
},
btctusdTicker.String(): {
Ticker: btctusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
btcusdProviderCfgOkx,
},
},
},
},
},
},
{
name: "multi market with multi provider",
input: mmtypes.MarketMap{
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): usdtusdMultiProvider,
btctusdTicker.String(): btcusdMultiProvider,
},
},
want: map[string]mmtypes.MarketMap{
"okx_ws": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): {
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgOkx,
},
},
btctusdTicker.String(): {
Ticker: btctusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
btcusdProviderCfgOkx,
},
},
},
},
"bitstamp_api": {
Markets: map[string]mmtypes.Market{
usdtusdTicker.String(): {
Ticker: usdtusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
usdtusdProviderCfgBitstamp,
},
},
btctusdTicker.String(): {
Ticker: btctusdTicker,
ProviderConfigs: []mmtypes.ProviderConfig{
btcusdProviderCfgBitstamp,
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := providertest.FilterMarketMapToProviders(tt.input)
require.Equal(t, tt.want, got)
})
}
}