diff --git a/protocol/Dockerfile b/protocol/Dockerfile index e72464440c..17a0bb9458 100644 --- a/protocol/Dockerfile +++ b/protocol/Dockerfile @@ -1,6 +1,6 @@ # NB: This is a digest for a multi-arch manifest list, you will want to get this by running # `docker buildx imagetools inspect golang:1.21-alpine` -ARG GOLANG_1_21_ALPINE_DIGEST="926f7f7e1ab8509b4e91d5ec6d5916ebb45155b0c8920291ba9f361d65385806" +ARG GOLANG_1_22_ALPINE_DIGEST="8e96e6cff6a388c2f70f5f662b64120941fcd7d4b89d62fec87520323a316bd9" # This Dockerfile is a stateless build of the `dydxprotocold` binary as a Docker container. # It does not include any configuration, state, or genesis information. @@ -9,7 +9,7 @@ ARG GOLANG_1_21_ALPINE_DIGEST="926f7f7e1ab8509b4e91d5ec6d5916ebb45155b0c8920291b # Builder # -------------------------------------------------------- -FROM golang@sha256:${GOLANG_1_21_ALPINE_DIGEST} as builder +FROM golang@sha256:${GOLANG_1_22_ALPINE_DIGEST} as builder ARG VERSION ARG COMMIT @@ -41,15 +41,24 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ -o /dydxprotocol/build/ \ ./... +# Build the oracle binary +WORKDIR / +RUN git clone https://github.com/skip-mev/slinky.git +WORKDIR /slinky +RUN make build + # -------------------------------------------------------- # Runner # -------------------------------------------------------- -FROM golang@sha256:${GOLANG_1_21_ALPINE_DIGEST} +FROM golang@sha256:${GOLANG_1_22_ALPINE_DIGEST} RUN apk add --no-cache bash COPY --from=builder /dydxprotocol/build/dydxprotocold /bin/dydxprotocold +COPY --from=builder /dydxprotocol/daemons/slinky/config/oracle.json /etc/oracle.json +COPY --from=builder /dydxprotocol/daemons/slinky/config/market.json /etc/market.json +COPY --from=builder /slinky/build/oracle /bin/slinky ENV HOME /dydxprotocol WORKDIR $HOME diff --git a/protocol/app/app.go b/protocol/app/app.go index 6f8fcc0a26..c631ef93a5 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -123,6 +123,7 @@ import ( bridgedaemontypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/bridge" liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations" pricefeedtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/pricefeed" + slinkyclient "github.com/dydxprotocol/v4-chain/protocol/daemons/slinky/client" daemontypes "github.com/dydxprotocol/v4-chain/protocol/daemons/types" // Modules @@ -202,6 +203,11 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "github.com/dydxprotocol/v4-chain/protocol/indexer/msgsender" + // Slinky + oracleconfig "github.com/skip-mev/slinky/oracle/config" + oracleclient "github.com/skip-mev/slinky/service/clients/oracle" + servicemetrics "github.com/skip-mev/slinky/service/metrics" + // Grpc Streaming streaming "github.com/dydxprotocol/v4-chain/protocol/streaming/grpc" streamingtypes "github.com/dydxprotocol/v4-chain/protocol/streaming/grpc/types" @@ -322,6 +328,9 @@ type App struct { BridgeClient *bridgeclient.Client DaemonHealthMonitor *daemonservertypes.HealthMonitor + + // Slinky + SlinkyClient *slinkyclient.Client } // assertAppPreconditions assert invariants required for an application to start. @@ -432,6 +441,9 @@ func New( if app.Server != nil { app.Server.Stop() } + if app.SlinkyClient != nil { + app.SlinkyClient.Stop() + } return nil }, ) @@ -766,25 +778,39 @@ func New( }() } - // Non-validating full-nodes have no need to run the price daemon. - if !appFlags.NonValidatingFullNode && daemonFlags.Price.Enabled { - exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) - // Start pricefeed client for sending prices for the pricefeed server to consume. These prices - // are retrieved via third-party APIs like Binance and then are encoded in-memory and - // periodically sent via gRPC to a shared socket with the server. - app.PriceFeedClient = pricefeedclient.StartNewClient( - // The client will use `context.Background` so that it can have a different context from - // the main application. - context.Background(), - daemonFlags, - appFlags, - logger, - &daemontypes.GrpcClientImpl{}, - exchangeQueryConfig, - constants.StaticExchangeDetails, - &pricefeedclient.SubTaskRunnerImpl{}, - ) - app.RegisterDaemonWithHealthMonitor(app.PriceFeedClient, maxDaemonUnhealthyDuration) + // Non-validating full-nodes have no need to run the oracle. + if !appFlags.NonValidatingFullNode { + if daemonFlags.Price.Enabled { + exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) + // Start pricefeed client for sending prices for the pricefeed server to consume. These prices + // are retrieved via third-party APIs like Binance and then are encoded in-memory and + // periodically sent via gRPC to a shared socket with the server. + app.PriceFeedClient = pricefeedclient.StartNewClient( + // The client will use `context.Background` so that it can have a different context from + // the main application. + context.Background(), + daemonFlags, + appFlags, + logger, + &daemontypes.GrpcClientImpl{}, + exchangeQueryConfig, + constants.StaticExchangeDetails, + &pricefeedclient.SubTaskRunnerImpl{}, + ) + app.RegisterDaemonWithHealthMonitor(app.PriceFeedClient, maxDaemonUnhealthyDuration) + } + if daemonFlags.Slinky.Enabled { + app.SlinkyClient = slinkyclient.StartNewClient( + context.Background(), + app.initSlinkySidecarClient(appOpts), + indexPriceCache, + &daemontypes.GrpcClientImpl{}, + daemonFlags, + appFlags, + logger, + ) + app.RegisterDaemonWithHealthMonitor(app.SlinkyClient, maxDaemonUnhealthyDuration) + } } // Start Bridge Daemon. @@ -1420,6 +1446,28 @@ func New( return app } +func (app *App) initSlinkySidecarClient(appOpts servertypes.AppOptions) oracleclient.OracleClient { + // Slinky setup + cfg, err := oracleconfig.ReadConfigFromAppOpts(appOpts) + if err != nil { + panic(err) + } + oracleMetrics, err := servicemetrics.NewMetricsFromConfig(cfg, app.ChainID()) + if err != nil { + panic(err) + } + // Create the oracle service. + slinkyClient, err := oracleclient.NewClientFromConfig( + cfg, + app.Logger().With("client", "oracle"), + oracleMetrics, + ) + if err != nil { + panic(err) + } + return slinkyClient +} + // RegisterDaemonWithHealthMonitor registers a daemon service with the update monitor, which will commence monitoring // the health of the daemon. If the daemon does not register, the method will panic. func (app *App) RegisterDaemonWithHealthMonitor( diff --git a/protocol/app/app_test.go b/protocol/app/app_test.go index 352ab0f2f7..1a55241e79 100644 --- a/protocol/app/app_test.go +++ b/protocol/app/app_test.go @@ -105,6 +105,7 @@ func TestAppIsFullyInitialized(t *testing.T) { "PriceFeedClient", "LiquidationsClient", "BridgeClient", + "SlinkyClient", // Any default constructed type can be considered initialized if the default is what is // expected. getUninitializedStructFields relies on fields being the non-default and diff --git a/protocol/cmd/dydxprotocold/cmd/config.go b/protocol/cmd/dydxprotocold/cmd/config.go index c95b8d0dbf..4124544c1b 100644 --- a/protocol/cmd/dydxprotocold/cmd/config.go +++ b/protocol/cmd/dydxprotocold/cmd/config.go @@ -7,6 +7,7 @@ import ( serverconfig "github.com/cosmos/cosmos-sdk/server/config" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + oracleconfig "github.com/skip-mev/slinky/oracle/config" ) const ( @@ -25,6 +26,7 @@ const ( // DydxAppConfig specifies dYdX app specific config. type DydxAppConfig struct { serverconfig.Config + Oracle oracleconfig.AppConfig `mapstructure:"oracle"` } // TODO(DEC-1718): Audit tendermint and app config parameters for mainnet. @@ -65,7 +67,7 @@ func initAppConfig() (string, *DydxAppConfig) { // GRPC. appConfig.GRPC.Address = "0.0.0.0:9090" - appTemplate := serverconfig.DefaultConfigTemplate + appTemplate := serverconfig.DefaultConfigTemplate + oracleconfig.DefaultConfigTemplate return appTemplate, &appConfig } diff --git a/protocol/daemons/flags/flags.go b/protocol/daemons/flags/flags.go index 3bae6c44e6..99dd8f804c 100644 --- a/protocol/daemons/flags/flags.go +++ b/protocol/daemons/flags/flags.go @@ -2,8 +2,10 @@ package flags import ( servertypes "github.com/cosmos/cosmos-sdk/server/types" + oracleconfig "github.com/skip-mev/slinky/oracle/config" "github.com/spf13/cast" "github.com/spf13/cobra" + "time" ) // List of CLI flags for Server and Client. @@ -23,6 +25,13 @@ const ( FlagLiquidationDaemonEnabled = "liquidation-daemon-enabled" FlagLiquidationDaemonLoopDelayMs = "liquidation-daemon-loop-delay-ms" FlagLiquidationDaemonQueryPageLimit = "liquidation-daemon-query-page-limit" + + // Oracle flags + FlagOracleEnabled = "oracle.enabled" + FlagOracleAddress = "oracle.oracle_address" + FlagOracleClientTimeout = "oracle.client_timeout" + FlagOracleMetricsEnabled = "oracle.metrics_enabled" + FlagOraclePrometheusServerAddress = "oracle.prometheus_server_address" ) // Shared flags contains configuration flags shared by all daemons. @@ -63,12 +72,17 @@ type PriceFlags struct { LoopDelayMs uint32 } +type SlinkyFlags struct { + oracleconfig.AppConfig +} + // DaemonFlags contains the collected configuration flags for all daemons. type DaemonFlags struct { Shared SharedFlags Bridge BridgeFlags Liquidation LiquidationFlags Price PriceFlags + Slinky SlinkyFlags } var defaultDaemonFlags *DaemonFlags @@ -96,6 +110,14 @@ func GetDefaultDaemonFlags() DaemonFlags { Enabled: true, LoopDelayMs: 3_000, }, + Slinky: SlinkyFlags{ + AppConfig: oracleconfig.AppConfig{ + OracleAddress: "localhost:8080", + ClientTimeout: time.Second * 2, + MetricsEnabled: false, + PrometheusServerAddress: "", + }, + }, } } return *defaultDaemonFlags @@ -173,6 +195,33 @@ func AddDaemonFlagsToCmd( df.Price.LoopDelayMs, "Delay in milliseconds between sending price updates to the application.", ) + + // Slinky Daemon. + cmd.Flags().Bool( + FlagOracleEnabled, + df.Slinky.AppConfig.Enabled, + "Enable the slinky oracle.", + ) + cmd.Flags().String( + FlagOracleAddress, + df.Slinky.AppConfig.OracleAddress, + "Address of the oracle sidecar.", + ) + cmd.Flags().Duration( + FlagOracleClientTimeout, + df.Slinky.AppConfig.ClientTimeout, + "Time out of the oracle sidecar client.", + ) + cmd.Flags().Bool( + FlagOracleMetricsEnabled, + df.Slinky.AppConfig.MetricsEnabled, + "Enable the oracle metrics reporting for Slinky.", + ) + cmd.Flags().String( + FlagOraclePrometheusServerAddress, + df.Slinky.AppConfig.PrometheusServerAddress, + "The address of the exposed prometheus address for Slinky metrics.", + ) } // GetDaemonFlagValuesFromOptions gets all daemon flag values from the `AppOptions` struct. @@ -245,5 +294,32 @@ func GetDaemonFlagValuesFromOptions( } } + // Slinky Daemon. + if option := appOpts.Get(FlagOracleEnabled); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.Slinky.AppConfig.Enabled = v + } + } + if option := appOpts.Get(FlagOracleAddress); option != nil { + if v, err := cast.ToStringE(option); err == nil { + result.Slinky.AppConfig.OracleAddress = v + } + } + if option := appOpts.Get(FlagOracleClientTimeout); option != nil { + if v, err := cast.ToDurationE(option); err == nil { + result.Slinky.AppConfig.ClientTimeout = v + } + } + if option := appOpts.Get(FlagOracleMetricsEnabled); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.Slinky.AppConfig.MetricsEnabled = v + } + } + if option := appOpts.Get(FlagOraclePrometheusServerAddress); option != nil { + if v, err := cast.ToStringE(option); err == nil { + result.Slinky.AppConfig.PrometheusServerAddress = v + } + } + return result } diff --git a/protocol/daemons/server/types/pricefeed/market_to_exchange_prices.go b/protocol/daemons/server/types/pricefeed/market_to_exchange_prices.go index 688633f624..4ce2fdbedf 100644 --- a/protocol/daemons/server/types/pricefeed/market_to_exchange_prices.go +++ b/protocol/daemons/server/types/pricefeed/market_to_exchange_prices.go @@ -57,8 +57,6 @@ func (mte *MarketToExchangePrices) UpdatePrices( // a price is valid iff // 1) the last update time is within a predefined threshold away from the given // read time. -// 2) the number of prices that meet 1) are greater than the minimum number of -// exchanges specified in the given input. func (mte *MarketToExchangePrices) GetValidMedianPrices( marketParams []types.MarketParam, readTime time.Time, @@ -101,26 +99,23 @@ func (mte *MarketToExchangePrices) GetValidMedianPrices( }, ) - // The number of valid prices must be >= min number of exchanges. - if len(validPrices) >= int(marketParam.MinExchanges) { - // Calculate the median. Returns an error if the input is empty. - median, err := lib.Median(validPrices) - if err != nil { - telemetry.IncrCounterWithLabels( - []string{ - metrics.PricefeedServer, - metrics.NoValidMedianPrice, - metrics.Count, - }, - 1, - []gometrics.Label{ - pricefeedmetrics.GetLabelForMarketId(marketId), - }, - ) - continue - } - marketIdToMedianPrice[marketId] = median + // Calculate the median. Returns an error if the input is empty. + median, err := lib.Median(validPrices) + if err != nil { + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedServer, + metrics.NoValidMedianPrice, + metrics.Count, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(marketId), + }, + ) + continue } + marketIdToMedianPrice[marketId] = median } return marketIdToMedianPrice diff --git a/protocol/daemons/server/types/pricefeed/market_to_exchange_prices_test.go b/protocol/daemons/server/types/pricefeed/market_to_exchange_prices_test.go index 4103479473..47d192f45d 100644 --- a/protocol/daemons/server/types/pricefeed/market_to_exchange_prices_test.go +++ b/protocol/daemons/server/types/pricefeed/market_to_exchange_prices_test.go @@ -158,12 +158,6 @@ func TestGetValidMedianPrices_EmptyResult(t *testing.T) { }, getPricesInputTime: constants.TimeT, }, - "Does not meet min exchanges": { - updatePriceInput: constants.AtTimeTPriceUpdate, - // MinExchanges is 3 for all markets, but updates are from 2 exchanges - getPricesInputMarketParams: constants.AllMarketParamsMinExchanges3, - getPricesInputTime: constants.TimeT, - }, } for name, tc := range tests { @@ -187,9 +181,7 @@ func TestGetValidMedianPrices_MultiMarketSuccess(t *testing.T) { r := mte.GetValidMedianPrices(constants.AllMarketParamsMinExchanges2, constants.TimeT) - require.Len(t, r, 2) + require.Len(t, r, 3) require.Equal(t, uint64(2002), r[constants.MarketId9]) // Median of 1001, 2002, 3003 require.Equal(t, uint64(2503), r[constants.MarketId8]) // Median of 2002, 3003 - // Market7 only has 1 valid price due to update time constraint, - // but the min exchanges required is 2. Therefore, no median price. } diff --git a/protocol/daemons/slinky/client/client.go b/protocol/daemons/slinky/client/client.go new file mode 100644 index 0000000000..cbce91fe7d --- /dev/null +++ b/protocol/daemons/slinky/client/client.go @@ -0,0 +1,162 @@ +package client + +import ( + "context" + "cosmossdk.io/errors" + "sync" + "time" + + "cosmossdk.io/log" + + oracleclient "github.com/skip-mev/slinky/service/clients/oracle" + + appflags "github.com/dydxprotocol/v4-chain/protocol/app/flags" + "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" + pricefeedtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/pricefeed" + daemontypes "github.com/dydxprotocol/v4-chain/protocol/daemons/types" + libtime "github.com/dydxprotocol/v4-chain/protocol/lib/time" +) + +// Ensure Client is HealthCheckable +var _ daemontypes.HealthCheckable = (*Client)(nil) + +// Client is the daemon implementation for pulling price data from the slinky sidecar. +type Client struct { + daemontypes.HealthCheckable + ctx context.Context + cf context.CancelFunc + marketPairFetcher MarketPairFetcher + priceFetcher PriceFetcher + wg sync.WaitGroup + logger log.Logger +} + +func newClient(ctx context.Context, logger log.Logger) *Client { + logger = logger.With(log.ModuleKey, SlinkyClientDaemonModuleName) + client := &Client{ + HealthCheckable: daemontypes.NewTimeBoundedHealthCheckable( + SlinkyClientDaemonModuleName, + &libtime.TimeProviderImpl{}, + logger, + ), + logger: logger, + } + client.ctx, client.cf = context.WithCancel(ctx) + return client +} + +// start creates the main goroutines of the Client. +func (c *Client) start( + slinky oracleclient.OracleClient, + indexPriceCache *pricefeedtypes.MarketToExchangePrices, + grpcClient daemontypes.GrpcClient, + appFlags appflags.Flags, +) error { + // 1. Start the MarketPairFetcher + c.marketPairFetcher = NewMarketPairFetcher(c.logger) + c.wg.Add(1) + go func() { + defer c.wg.Done() + c.RunMarketPairFetcher(c.ctx, appFlags, grpcClient) + }() + // 2. Start the PriceFetcher + c.priceFetcher = NewPriceFetcher( + c.marketPairFetcher, + indexPriceCache, + slinky, + c.logger, + ) + c.wg.Add(1) + go func() { + defer c.wg.Done() + c.RunPriceFetcher(c.ctx) + }() + return nil +} + +// RunPriceFetcher periodically calls the priceFetcher to grab prices from the slinky sidecar and +// push them to the pricefeed server. +func (c *Client) RunPriceFetcher(ctx context.Context) { + err := c.priceFetcher.Start(ctx) + if err != nil { + c.logger.Error("Error initializing PriceFetcher in slinky daemon: %w", err) + panic(err) + } + ticker := time.NewTicker(SlinkyPriceFetchDelay) + defer ticker.Stop() + for { + select { + case <-ticker.C: + err := c.priceFetcher.FetchPrices(ctx) + if err != nil { + c.logger.Error("Failed to run fetch prices for slinky daemon", "error", err) + c.ReportFailure(errors.Wrap(err, "failed to run PriceFetcher for slinky daemon")) + } else { + c.ReportSuccess() + } + case <-ctx.Done(): + return + } + } +} + +// Stop closes all connections and waits for goroutines to exit. +func (c *Client) Stop() { + c.cf() + c.priceFetcher.Stop() + c.marketPairFetcher.Stop() + c.wg.Wait() +} + +// RunMarketPairFetcher periodically calls the marketPairFetcher to cache mappings between +// currency pair and market param ID. +func (c *Client) RunMarketPairFetcher(ctx context.Context, appFlags appflags.Flags, grpcClient daemontypes.GrpcClient) { + err := c.marketPairFetcher.Start(ctx, appFlags, grpcClient) + if err != nil { + c.logger.Error("Error initializing MarketPairFetcher in slinky daemon: %w", err) + panic(err) + } + ticker := time.NewTicker(SlinkyMarketParamFetchDelay) + defer ticker.Stop() + for { + select { + case <-ticker.C: + err = c.marketPairFetcher.FetchIdMappings(ctx) + if err != nil { + c.logger.Error("Failed to run fetch id mappings for slinky daemon", "error", err) + c.ReportFailure(errors.Wrap(err, "failed to run FetchIdMappings for slinky daemon")) + } + c.ReportSuccess() + case <-ctx.Done(): + return + } + } +} + +// StartNewClient creates and runs a Client. +// The client creates the MarketPairFetcher and PriceFetcher, +// connects to the required grpc services, and launches them in goroutines. +// It is non-blocking and returns on successful startup. +// If it hits a critical error in startup it panics. +func StartNewClient( + ctx context.Context, + slinky oracleclient.OracleClient, + indexPriceCache *pricefeedtypes.MarketToExchangePrices, + grpcClient daemontypes.GrpcClient, + daemonFlags flags.DaemonFlags, + appFlags appflags.Flags, + logger log.Logger, +) *Client { + logger.Info( + "Starting slinky daemon with flags", + "SlinkyFlags", daemonFlags.Slinky, + ) + + client := newClient(ctx, logger) + err := client.start(slinky, indexPriceCache, grpcClient, appFlags) + if err != nil { + logger.Error("Error initializing slinky daemon: %w", err) + panic(err) + } + return client +} diff --git a/protocol/daemons/slinky/client/client_test.go b/protocol/daemons/slinky/client/client_test.go new file mode 100644 index 0000000000..e8d234f870 --- /dev/null +++ b/protocol/daemons/slinky/client/client_test.go @@ -0,0 +1,106 @@ +package client_test + +import ( + "context" + "net" + "sync" + "testing" + "time" + + "cosmossdk.io/log" + "github.com/skip-mev/slinky/service/servers/oracle/types" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + + appflags "github.com/dydxprotocol/v4-chain/protocol/app/flags" + daemonflags "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" + pricefeedtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/pricefeed" + "github.com/dydxprotocol/v4-chain/protocol/daemons/slinky/client" + daemontypes "github.com/dydxprotocol/v4-chain/protocol/daemons/types" + "github.com/dydxprotocol/v4-chain/protocol/mocks" + "github.com/dydxprotocol/v4-chain/protocol/testutil/appoptions" + pricetypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" +) + +func TestClientTestSuite(t *testing.T) { + suite.Run(t, &ClientTestSuite{}) +} + +type ClientTestSuite struct { + suite.Suite + daemonFlags daemonflags.DaemonFlags + appFlags appflags.Flags + grpcServer *grpc.Server + pricesMockQueryServer *mocks.QueryServer + wg sync.WaitGroup +} + +func (c *ClientTestSuite) SetupTest() { + // Setup grpc server. + c.daemonFlags = daemonflags.GetDefaultDaemonFlags() + c.appFlags = appflags.GetFlagValuesFromOptions(appoptions.GetDefaultTestAppOptions("", nil)) + c.grpcServer = grpc.NewServer() + + c.pricesMockQueryServer = &mocks.QueryServer{} + pricetypes.RegisterQueryServer(c.grpcServer, c.pricesMockQueryServer) + + c.wg.Add(1) + go func() { + defer c.wg.Done() + ls, err := net.Listen("tcp", c.appFlags.GrpcAddress) + c.Require().NoError(err) + _ = c.grpcServer.Serve(ls) + }() +} + +func (c *ClientTestSuite) TearDownTest() { + c.grpcServer.Stop() + c.wg.Wait() +} + +func (c *ClientTestSuite) TestClient() { + var cli *client.Client + slinky := mocks.NewOracleClient(c.T()) + logger := log.NewTestLogger(c.T()) + + c.pricesMockQueryServer.On("AllMarketParams", mock.Anything, mock.Anything). + Return( + &pricetypes.QueryAllMarketParamsResponse{ + MarketParams: []pricetypes.MarketParam{ + {Id: 0, Pair: "FOO-BAR"}, + {Id: 1, Pair: "BAR-FOO"}, + }}, + nil, + ) + + c.Run("services are all started and call their deps", func() { + slinky.On("Stop").Return(nil) + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{ + Prices: map[string]string{ + "FOO/BAR": "100000000000", + }, + Timestamp: time.Now(), + }, nil) + client.SlinkyPriceFetchDelay = time.Millisecond + client.SlinkyMarketParamFetchDelay = time.Millisecond + cli = client.StartNewClient( + context.Background(), + slinky, + pricefeedtypes.NewMarketToExchangePrices(5*time.Second), + &daemontypes.GrpcClientImpl{}, + c.daemonFlags, + c.appFlags, + logger, + ) + waitTime := time.Second * 5 + c.Require().Eventually(func() bool { + return cli.HealthCheck() == nil + }, waitTime, time.Millisecond*500, "Slinky daemon failed to become healthy within %s", waitTime) + // Need to wait until a single c + cli.Stop() + c.Require().NoError(cli.HealthCheck()) + }) +} diff --git a/protocol/daemons/slinky/client/constants.go b/protocol/daemons/slinky/client/constants.go new file mode 100644 index 0000000000..f69c43c84e --- /dev/null +++ b/protocol/daemons/slinky/client/constants.go @@ -0,0 +1,20 @@ +package client + +import "time" + +var ( + // SlinkyPriceServerConnectionTimeout controls the timeout of establishing a + // grpc connection to the pricefeed server. + SlinkyPriceServerConnectionTimeout = time.Second * 5 + // SlinkyPriceFetchDelay controls the frequency at which we pull prices from slinky and push + // them to the pricefeed server. + SlinkyPriceFetchDelay = time.Second * 2 + // SlinkyMarketParamFetchDelay is the frequency at which we query the x/price module to refresh mappings from + // currency pair to x/price ID. + SlinkyMarketParamFetchDelay = time.Millisecond * 1900 +) + +const ( + // SlinkyClientDaemonModuleName is the module name used in logging. + SlinkyClientDaemonModuleName = "slinky-client-daemon" +) diff --git a/protocol/daemons/slinky/client/market_pair_fetcher.go b/protocol/daemons/slinky/client/market_pair_fetcher.go new file mode 100644 index 0000000000..c80169304a --- /dev/null +++ b/protocol/daemons/slinky/client/market_pair_fetcher.go @@ -0,0 +1,108 @@ +package client + +import ( + "context" + "fmt" + "sync" + + "cosmossdk.io/log" + "google.golang.org/grpc" + + oracletypes "github.com/skip-mev/slinky/x/oracle/types" + + appflags "github.com/dydxprotocol/v4-chain/protocol/app/flags" + daemontypes "github.com/dydxprotocol/v4-chain/protocol/daemons/types" + "github.com/dydxprotocol/v4-chain/protocol/lib/slinky" + pricetypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" +) + +// MarketPairFetcher is a lightweight process run in a goroutine by the slinky client. +// Its purpose is to periodically query the prices module for MarketParams and create +// an easily indexed mapping between Slinky's CurrencyPair type and the corresponding ID +// in the x/prices module. +type MarketPairFetcher interface { + Start(context.Context, appflags.Flags, daemontypes.GrpcClient) error + Stop() + GetIDForPair(oracletypes.CurrencyPair) (uint32, error) + FetchIdMappings(context.Context) error +} + +// MarketPairFetcherImpl implements the MarketPairFetcher interface. +type MarketPairFetcherImpl struct { + Logger log.Logger + QueryConn *grpc.ClientConn + PricesQueryClient pricetypes.QueryClient + + // compatMappings stores a mapping between CurrencyPair and the corresponding market(param|price) ID + compatMappings map[oracletypes.CurrencyPair]uint32 + compatMu sync.RWMutex +} + +func NewMarketPairFetcher(logger log.Logger) MarketPairFetcher { + return &MarketPairFetcherImpl{ + Logger: logger, + compatMappings: make(map[oracletypes.CurrencyPair]uint32), + } +} + +// Start opens the grpc connections for the fetcher. +func (m *MarketPairFetcherImpl) Start( + ctx context.Context, + appFlags appflags.Flags, + grpcClient daemontypes.GrpcClient) error { + // Create the query client connection + queryConn, err := grpcClient.NewTcpConnection(ctx, appFlags.GrpcAddress) + if err != nil { + m.Logger.Error( + "Failed to establish gRPC connection", + "gRPC address", appFlags.GrpcAddress, + "error", err, + ) + return err + } + m.PricesQueryClient = pricetypes.NewQueryClient(queryConn) + return nil +} + +// Stop closes all existing connections. +func (m *MarketPairFetcherImpl) Stop() { + if m.QueryConn != nil { + _ = m.QueryConn.Close() + } +} + +// GetIDForPair returns the ID corresponding to the currency pair in the x/prices module. +// If the currency pair is not found it will return an error. +func (m *MarketPairFetcherImpl) GetIDForPair(cp oracletypes.CurrencyPair) (uint32, error) { + var id uint32 + m.compatMu.RLock() + defer m.compatMu.RUnlock() + id, found := m.compatMappings[cp] + if !found { + return id, fmt.Errorf("pair %s not found in compatMappings", cp.String()) + } + return id, nil +} + +// FetchIdMappings is run periodically to refresh the cache of known mappings between +// CurrencyPair and MarketParam ID. +func (m *MarketPairFetcherImpl) FetchIdMappings(ctx context.Context) error { + // fetch all market params + resp, err := m.PricesQueryClient.AllMarketParams(ctx, &pricetypes.QueryAllMarketParamsRequest{}) + if err != nil { + return err + } + var compatMappings = make(map[oracletypes.CurrencyPair]uint32, len(resp.MarketParams)) + for _, mp := range resp.MarketParams { + cp, err := slinky.MarketPairToCurrencyPair(mp.Pair) + if err != nil { + return err + } + m.Logger.Debug("Mapped market to pair", "market id", mp.Id, "currency pair", cp.String()) + compatMappings[cp] = mp.Id + } + m.compatMu.Lock() + defer m.compatMu.Unlock() + m.compatMappings = compatMappings + return nil +} diff --git a/protocol/daemons/slinky/client/market_pair_fetcher_test.go b/protocol/daemons/slinky/client/market_pair_fetcher_test.go new file mode 100644 index 0000000000..18c511ec82 --- /dev/null +++ b/protocol/daemons/slinky/client/market_pair_fetcher_test.go @@ -0,0 +1,91 @@ +package client_test + +import ( + "context" + "fmt" + "testing" + + "cosmossdk.io/log" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + slinkytypes "github.com/skip-mev/slinky/x/oracle/types" + + "github.com/dydxprotocol/v4-chain/protocol/daemons/slinky/client" + "github.com/dydxprotocol/v4-chain/protocol/mocks" + "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" +) + +func TestMarketPairFetcher(t *testing.T) { + logger := log.NewTestLogger(t) + queryClient := mocks.NewQueryClient(t) + fetcher := client.MarketPairFetcherImpl{ + Logger: logger, + PricesQueryClient: queryClient, + } + asset0 := "FOO" + asset1 := "BAR" + pair0 := types.MarketParam{Id: 0, Pair: fmt.Sprintf("%s-%s", asset0, asset1)} + pair1 := types.MarketParam{Id: 1, Pair: fmt.Sprintf("%s-%s", asset1, asset0)} + invalidPair := types.MarketParam{Id: 2, Pair: "foobar"} + + t.Run("caches and returns valid pairs", func(t *testing.T) { + queryClient. + On("AllMarketParams", mock.Anything, mock.Anything). + Return( + &types.QueryAllMarketParamsResponse{ + MarketParams: []types.MarketParam{ + pair0, + pair1, + }}, + nil, + ).Once() + err := fetcher.FetchIdMappings(context.Background()) + require.NoError(t, err) + id, err := fetcher.GetIDForPair(slinkytypes.CurrencyPair{Base: asset0, Quote: asset1}) + require.NoError(t, err) + require.Equal(t, pair0.Id, id) + id, err = fetcher.GetIDForPair(slinkytypes.CurrencyPair{Base: asset1, Quote: asset0}) + require.NoError(t, err) + require.Equal(t, pair1.Id, id) + }) + + t.Run("errors on fetch non-cached pair", func(t *testing.T) { + queryClient. + On("AllMarketParams", mock.Anything, mock.Anything). + Return( + &types.QueryAllMarketParamsResponse{ + MarketParams: []types.MarketParam{}}, + nil, + ).Once() + err := fetcher.FetchIdMappings(context.Background()) + require.NoError(t, err) + _, err = fetcher.GetIDForPair(slinkytypes.CurrencyPair{Base: asset0, Quote: asset1}) + require.Error(t, err, fmt.Errorf("pair %s/%s not found in compatMappings", asset0, asset1)) + }) + + t.Run("fails on fetching invalid pairs", func(t *testing.T) { + queryClient. + On("AllMarketParams", mock.Anything, mock.Anything). + Return( + &types.QueryAllMarketParamsResponse{ + MarketParams: []types.MarketParam{ + invalidPair, + }}, + nil, + ).Once() + err := fetcher.FetchIdMappings(context.Background()) + require.Error(t, err, "incorrectly formatted CurrencyPair: foobar") + }) + + t.Run("fails on prices query error", func(t *testing.T) { + queryClient. + On("AllMarketParams", mock.Anything, mock.Anything). + Return( + &types.QueryAllMarketParamsResponse{}, + fmt.Errorf("test error"), + ).Once() + err := fetcher.FetchIdMappings(context.Background()) + require.Error(t, err, "test error") + }) +} diff --git a/protocol/daemons/slinky/client/price_fetcher.go b/protocol/daemons/slinky/client/price_fetcher.go new file mode 100644 index 0000000000..32a663e5ad --- /dev/null +++ b/protocol/daemons/slinky/client/price_fetcher.go @@ -0,0 +1,118 @@ +package client + +import ( + "context" + "strconv" + + "cosmossdk.io/log" + oracleclient "github.com/skip-mev/slinky/service/clients/oracle" + "github.com/skip-mev/slinky/service/servers/oracle/types" + oracletypes "github.com/skip-mev/slinky/x/oracle/types" + + "github.com/dydxprotocol/v4-chain/protocol/daemons/pricefeed/api" + pricefeedtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/pricefeed" +) + +// PriceFetcher is responsible for pulling prices from the slinky sidecar and sending them to the pricefeed server. +type PriceFetcher interface { + Start(ctx context.Context) error + Stop() + FetchPrices(ctx context.Context) error +} + +// PriceFetcherImpl implements the PriceFetcher interface. +type PriceFetcherImpl struct { + marketPairFetcher MarketPairFetcher + indexPriceCache *pricefeedtypes.MarketToExchangePrices + slinky oracleclient.OracleClient + logger log.Logger +} + +// NewPriceFetcher creates a PriceFetcher. +func NewPriceFetcher( + marketPairFetcher MarketPairFetcher, + indexPriceCache *pricefeedtypes.MarketToExchangePrices, + slinky oracleclient.OracleClient, + logger log.Logger) PriceFetcher { + return &PriceFetcherImpl{ + marketPairFetcher: marketPairFetcher, + indexPriceCache: indexPriceCache, + slinky: slinky, + logger: logger, + } +} + +// Start initializes the underlying connections of the PriceFetcher. +func (p *PriceFetcherImpl) Start(ctx context.Context) error { + return p.slinky.Start(ctx) +} + +// Stop closes all open connections. +func (p *PriceFetcherImpl) Stop() { + _ = p.slinky.Stop() +} + +// FetchPrices pulls prices from Slinky, translates the returned data format to dydx-compatible types, +// and sends the price updates to the index price cache via the pricefeed server. +// It uses the MarketPairFetcher to efficiently map between Slinky's CurrencyPair primary key and dydx's +// MarketParam (or MarketPrice) ID. +// +// The markets in the index price cache will only have a single index price (from slinky). +// This is because the sidecar pre-aggregates market data. +func (p *PriceFetcherImpl) FetchPrices(ctx context.Context) error { + // get prices from slinky sidecar via GRPC + slinkyResponse, err := p.slinky.Prices(ctx, &types.QueryPricesRequest{}) + if err != nil { + return err + } + + // update the prices keeper w/ the most recent prices for the relevant markets + var updates []*api.MarketPriceUpdate + for currencyPairString, priceString := range slinkyResponse.Prices { + // convert currency-pair string (index) into currency-pair object + currencyPair, err := oracletypes.CurrencyPairFromString(currencyPairString) + if err != nil { + return err + } + p.logger.Info("turned price pair to currency pair", + "string", currencyPairString, + "currency pair", currencyPair.String()) + + // get the market id for the currency pair + id, err := p.marketPairFetcher.GetIDForPair(currencyPair) + if err != nil { + p.logger.Info("slinky client returned currency pair not found in MarketPairFetcher", + "currency pair", currencyPairString, + "error", err) + continue + } + + // parse the price string into a uint64 + price, err := strconv.ParseUint(priceString, 10, 64) + if err != nil { + p.logger.Error("slinky client returned a price not parsable as uint64", "price", priceString) + continue + } + p.logger.Info("parsed update for", "market id", id, "price", price) + + // append the update to the list of MarketPriceUpdates to be sent to the app's price-feed service + updates = append(updates, &api.MarketPriceUpdate{ + MarketId: id, + ExchangePrices: []*api.ExchangePrice{ + { + ExchangeId: "slinky", + Price: price, + LastUpdateTime: &slinkyResponse.Timestamp, + }, + }, + }) + } + + // send the updates to the indexPriceCache + if len(updates) == 0 { + p.logger.Info("Slinky returned 0 valid market price updates") + return nil + } + p.indexPriceCache.UpdatePrices(updates) + return nil +} diff --git a/protocol/daemons/slinky/client/price_fetcher_test.go b/protocol/daemons/slinky/client/price_fetcher_test.go new file mode 100644 index 0000000000..647d6a544d --- /dev/null +++ b/protocol/daemons/slinky/client/price_fetcher_test.go @@ -0,0 +1,209 @@ +package client_test + +import ( + "context" + "fmt" + "net" + "sync" + "testing" + "time" + + "cosmossdk.io/log" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + + "github.com/skip-mev/slinky/service/servers/oracle/types" + + appflags "github.com/dydxprotocol/v4-chain/protocol/app/flags" + daemonflags "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" + pricefeed_types "github.com/dydxprotocol/v4-chain/protocol/daemons/pricefeed/types" + daemonserver "github.com/dydxprotocol/v4-chain/protocol/daemons/server" + pricefeedserver_types "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/pricefeed" + "github.com/dydxprotocol/v4-chain/protocol/daemons/slinky/client" + daemontypes "github.com/dydxprotocol/v4-chain/protocol/daemons/types" + "github.com/dydxprotocol/v4-chain/protocol/mocks" + "github.com/dydxprotocol/v4-chain/protocol/testutil/appoptions" +) + +func TestPriceFetcherTestSuite(t *testing.T) { + suite.Run(t, &PriceFetcherTestSuite{}) +} + +type PriceFetcherTestSuite struct { + suite.Suite + daemonFlags daemonflags.DaemonFlags + appFlags appflags.Flags + daemonServer *daemonserver.Server + pricesGrpcServer *grpc.Server + wg sync.WaitGroup +} + +func (p *PriceFetcherTestSuite) SetupTest() { + // Setup daemon and grpc servers. + p.daemonFlags = daemonflags.GetDefaultDaemonFlags() + p.appFlags = appflags.GetFlagValuesFromOptions(appoptions.GetDefaultTestAppOptions("", nil)) + + // Configure and run daemon server. + p.daemonServer = daemonserver.NewServer( + log.NewNopLogger(), + grpc.NewServer(), + &daemontypes.FileHandlerImpl{}, + p.daemonFlags.Shared.SocketAddress, + ) + p.daemonServer.WithPriceFeedMarketToExchangePrices( + pricefeedserver_types.NewMarketToExchangePrices(5 * time.Second), + ) + + p.wg.Add(1) + go func() { + defer p.wg.Done() + p.daemonServer.Start() + }() + + // Create a gRPC server running on the default port and attach the mock prices query response. + p.pricesGrpcServer = grpc.NewServer() + + p.wg.Add(1) + go func() { + defer p.wg.Done() + ls, err := net.Listen("tcp", p.appFlags.GrpcAddress) + p.Require().NoError(err) + _ = p.pricesGrpcServer.Serve(ls) + }() +} + +func (p *PriceFetcherTestSuite) TearDownTest() { + p.daemonServer.Stop() + p.pricesGrpcServer.Stop() + p.wg.Wait() +} + +func (p *PriceFetcherTestSuite) TestPriceFetcher() { + logger := log.NewTestLogger(p.T()) + mpf := mocks.NewMarketPairFetcher(p.T()) + slinky := mocks.NewOracleClient(p.T()) + slinky.On("Stop").Return(nil) + var fetcher client.PriceFetcher + + p.Run("fetches prices on valid inputs", func() { + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{ + Prices: map[string]string{ + "FOO/BAR": "100000000000", + }, + Timestamp: time.Now(), + }, nil).Once() + mpf.On("GetIDForPair", mock.Anything).Return(uint32(1), nil).Once() + + fetcher = client.NewPriceFetcher( + mpf, + pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), + slinky, + logger, + ) + p.Require().NoError(fetcher.Start(context.Background())) + p.Require().NoError(fetcher.FetchPrices(context.Background())) + fetcher.Stop() + }) + + p.Run("errors on slinky.Prices failure", func() { + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{}, fmt.Errorf("foobar")).Once() + fetcher = client.NewPriceFetcher( + mpf, + pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), + slinky, + logger, + ) + + p.Require().NoError(fetcher.Start(context.Background())) + p.Require().Errorf(fetcher.FetchPrices(context.Background()), "foobar") + fetcher.Stop() + }) + + p.Run("errors on slinky.Prices returning invalid currency pairs", func() { + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{ + Prices: map[string]string{ + "FOOBAR": "100000000000", + }, + }, nil).Once() + fetcher = client.NewPriceFetcher( + mpf, + pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), + slinky, + logger, + ) + + p.Require().NoError(fetcher.Start(context.Background())) + p.Require().Errorf(fetcher.FetchPrices(context.Background()), "incorrectly formatted CurrencyPair") + fetcher.Stop() + }) + + p.Run("no-ops on marketPairFetcher currency pair not found", func() { + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{ + Prices: map[string]string{ + "FOO/BAR": "100000000000", + }, + Timestamp: time.Now(), + }, nil).Once() + mpf.On("GetIDForPair", mock.Anything).Return(uint32(1), fmt.Errorf("not found")).Once() + + fetcher = client.NewPriceFetcher( + mpf, + pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), + slinky, + logger, + ) + p.Require().NoError(fetcher.Start(context.Background())) + p.Require().NoError(fetcher.FetchPrices(context.Background())) + fetcher.Stop() + }) + + p.Run("continues on non-parsable price data", func() { + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{ + Prices: map[string]string{ + "FOO/BAR": "abc123", + }, + Timestamp: time.Now(), + }, nil).Once() + mpf.On("GetIDForPair", mock.Anything).Return(uint32(1), nil).Once() + + fetcher = client.NewPriceFetcher( + mpf, + pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), + slinky, + logger, + ) + p.Require().NoError(fetcher.Start(context.Background())) + p.Require().NoError(fetcher.FetchPrices(context.Background())) + fetcher.Stop() + }) + + p.Run("no-ops on empty price response", func() { + slinky.On("Start", mock.Anything).Return(nil).Once() + slinky.On("Prices", mock.Anything, mock.Anything). + Return(&types.QueryPricesResponse{ + Prices: map[string]string{}, + Timestamp: time.Now(), + }, nil).Once() + + fetcher = client.NewPriceFetcher( + mpf, + pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), + slinky, + logger, + ) + p.Require().NoError(fetcher.Start(context.Background())) + p.Require().NoError(fetcher.FetchPrices(context.Background())) + fetcher.Stop() + }) +} diff --git a/protocol/daemons/slinky/config/market.json b/protocol/daemons/slinky/config/market.json new file mode 100644 index 0000000000..d4433a03a2 --- /dev/null +++ b/protocol/daemons/slinky/config/market.json @@ -0,0 +1,2803 @@ +{ + "tickers": { + "ADA/USD": { + "currency_pair": { + "Base": "ADA", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ADA/USDC": { + "currency_pair": { + "Base": "ADA", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ADA/USDT": { + "currency_pair": { + "Base": "ADA", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "APE/USD": { + "currency_pair": { + "Base": "APE", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "APE/USDC": { + "currency_pair": { + "Base": "APE", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "APE/USDT": { + "currency_pair": { + "Base": "APE", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "APT/USD": { + "currency_pair": { + "Base": "APT", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "APT/USDC": { + "currency_pair": { + "Base": "APT", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "APT/USDT": { + "currency_pair": { + "Base": "APT", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ARB/USD": { + "currency_pair": { + "Base": "ARB", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ARB/USDT": { + "currency_pair": { + "Base": "ARB", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ATOM/USD": { + "currency_pair": { + "Base": "ATOM", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ATOM/USDC": { + "currency_pair": { + "Base": "ATOM", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ATOM/USDT": { + "currency_pair": { + "Base": "ATOM", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "AVAX/USD": { + "currency_pair": { + "Base": "AVAX", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "AVAX/USDC": { + "currency_pair": { + "Base": "AVAX", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "AVAX/USDT": { + "currency_pair": { + "Base": "AVAX", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BCH/USD": { + "currency_pair": { + "Base": "BCH", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BCH/USDT": { + "currency_pair": { + "Base": "BCH", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BLUR/USD": { + "currency_pair": { + "Base": "BLUR", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BLUR/USDT": { + "currency_pair": { + "Base": "BLUR", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BTC/USD": { + "currency_pair": { + "Base": "BTC", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BTC/USDC": { + "currency_pair": { + "Base": "BTC", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "BTC/USDT": { + "currency_pair": { + "Base": "BTC", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "COMP/USD": { + "currency_pair": { + "Base": "COMP", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "COMP/USDT": { + "currency_pair": { + "Base": "COMP", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "CRV/USD": { + "currency_pair": { + "Base": "CRV", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "CRV/USDT": { + "currency_pair": { + "Base": "CRV", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "DOGE/USD": { + "currency_pair": { + "Base": "DOGE", + "Quote": "USD" + }, + "decimals": 18, + "min_provider_count": 1 + }, + "DOGE/USDT": { + "currency_pair": { + "Base": "DOGE", + "Quote": "USDT" + }, + "decimals": 18, + "min_provider_count": 1 + }, + "DOT/USD": { + "currency_pair": { + "Base": "DOT", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "DOT/USDT": { + "currency_pair": { + "Base": "DOT", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "DYDX/USD": { + "currency_pair": { + "Base": "DYDX", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "DYDX/USDC": { + "currency_pair": { + "Base": "DYDX", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "DYDX/USDT": { + "currency_pair": { + "Base": "DYDX", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ETC/USD": { + "currency_pair": { + "Base": "ETC", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ETC/USDT": { + "currency_pair": { + "Base": "ETC", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ETH/BTC": { + "currency_pair": { + "Base": "ETH", + "Quote": "BTC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ETH/USD": { + "currency_pair": { + "Base": "ETH", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ETH/USDC": { + "currency_pair": { + "Base": "ETH", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "ETH/USDT": { + "currency_pair": { + "Base": "ETH", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "FIL/USD": { + "currency_pair": { + "Base": "FIL", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "FIL/USDT": { + "currency_pair": { + "Base": "FIL", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "LDO/USD": { + "currency_pair": { + "Base": "LDO", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "LDO/USDT": { + "currency_pair": { + "Base": "LDO", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "LINK/USD": { + "currency_pair": { + "Base": "LINK", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "LINK/USDT": { + "currency_pair": { + "Base": "LINK", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "LTC/USD": { + "currency_pair": { + "Base": "LTC", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "LTC/USDT": { + "currency_pair": { + "Base": "LTC", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "MATIC/USD": { + "currency_pair": { + "Base": "MATIC", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "MATIC/USDT": { + "currency_pair": { + "Base": "MATIC", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "MKR/USD": { + "currency_pair": { + "Base": "MKR", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "MKR/USDT": { + "currency_pair": { + "Base": "MKR", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "NEAR/USD": { + "currency_pair": { + "Base": "NEAR", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "NEAR/USDT": { + "currency_pair": { + "Base": "NEAR", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "OP/USD": { + "currency_pair": { + "Base": "OP", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "OP/USDT": { + "currency_pair": { + "Base": "OP", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "OSMO/USD": { + "currency_pair": { + "Base": "OSMO", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "OSMO/USDC": { + "currency_pair": { + "Base": "OSMO", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "OSMO/USDT": { + "currency_pair": { + "Base": "OSMO", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "PEPE/USD": { + "currency_pair": { + "Base": "PEPE", + "Quote": "USD" + }, + "decimals": 18, + "min_provider_count": 1 + }, + "PEPE/USDT": { + "currency_pair": { + "Base": "PEPE", + "Quote": "USDT" + }, + "decimals": 18, + "min_provider_count": 1 + }, + "SEI/USD": { + "currency_pair": { + "Base": "SEI", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "SEI/USDT": { + "currency_pair": { + "Base": "SEI", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "SHIB/USD": { + "currency_pair": { + "Base": "SHIB", + "Quote": "USD" + }, + "decimals": 18, + "min_provider_count": 1 + }, + "SHIB/USDT": { + "currency_pair": { + "Base": "SHIB", + "Quote": "USDT" + }, + "decimals": 18, + "min_provider_count": 1 + }, + "SOL/USD": { + "currency_pair": { + "Base": "SOL", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "SOL/USDC": { + "currency_pair": { + "Base": "SOL", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "SOL/USDT": { + "currency_pair": { + "Base": "SOL", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "SUI/USD": { + "currency_pair": { + "Base": "SUI", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "SUI/USDT": { + "currency_pair": { + "Base": "SUI", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "TIA/USD": { + "currency_pair": { + "Base": "TIA", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "TIA/USDC": { + "currency_pair": { + "Base": "TIA", + "Quote": "USDC" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "TIA/USDT": { + "currency_pair": { + "Base": "TIA", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "TRX/USD": { + "currency_pair": { + "Base": "TRX", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "TRX/USDT": { + "currency_pair": { + "Base": "TRX", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "UNI/USD": { + "currency_pair": { + "Base": "UNI", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "UNI/USDT": { + "currency_pair": { + "Base": "UNI", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "USDC/USD": { + "currency_pair": { + "Base": "USDC", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "USDC/USDT": { + "currency_pair": { + "Base": "USDC", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "USDT/USD": { + "currency_pair": { + "Base": "USDT", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "WLD/USDT": { + "currency_pair": { + "Base": "WLD", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "XLM/USD": { + "currency_pair": { + "Base": "XLM", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "XLM/USDT": { + "currency_pair": { + "Base": "XLM", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "XRP/USD": { + "currency_pair": { + "Base": "XRP", + "Quote": "USD" + }, + "decimals": 8, + "min_provider_count": 1 + }, + "XRP/USDT": { + "currency_pair": { + "Base": "XRP", + "Quote": "USDT" + }, + "decimals": 8, + "min_provider_count": 1 + } + }, + "paths": {}, + "providers": { + "ADA/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "ADA-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "ADA-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "ADA/USD" + }, + { + "name": "okx", + "off_chain_ticker": "ADA-USD" + } + ] + }, + "ADA/USDC": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ADA-USDC" + }, + { + "name": "mexc", + "off_chain_ticker": "ADAUSDC" + }, + { + "name": "okx", + "off_chain_ticker": "ADA-USDC" + } + ] + }, + "ADA/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ADA-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "ADAUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "ADAUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "ADA_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "adausdt" + }, + { + "name": "okx", + "off_chain_ticker": "ADA-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "ADAUSDT" + } + ] + }, + "APE/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "APE-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "APE-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "APE/USD" + } + ] + }, + "APE/USDC": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "APE-USDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "APE-USDC" + }, + { + "name": "okx", + "off_chain_ticker": "APE-USDC" + } + ] + }, + "APE/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "APE-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "APEUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "APE-USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "APE_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "APE-USDT" + }, + { + "name": "okx", + "off_chain_ticker": "APE-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "APEUSDT" + } + ] + }, + "APT/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "APT-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "APT-USD" + } + ] + }, + "APT/USDC": { + "providers": [ + { + "name": "okx", + "off_chain_ticker": "APT-USDC" + } + ] + }, + "APT/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "APT-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "APTUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "APTUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "APT_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "aptusdt" + }, + { + "name": "okx", + "off_chain_ticker": "APT-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "APTUSDT" + } + ] + }, + "ARB/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "ARB-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "ARB-USD" + } + ] + }, + "ARB/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ARB-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "ARBUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "ARBUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "ARB_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "arbusdt" + }, + { + "name": "okx", + "off_chain_ticker": "ARB-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "ARBUSDT" + } + ] + }, + "ATOM/USD": { + "providers": [ + { + "name": "coingecko", + "off_chain_ticker": "cosmos/usd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "ATOM-USD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "ATOMUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "ATOM-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "ATOM/USD" + }, + { + "name": "okx", + "off_chain_ticker": "ATOM-USD" + } + ] + }, + "ATOM/USDC": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ATOM-USDC" + }, + { + "name": "mexc", + "off_chain_ticker": "ATOMUSDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "ATOM-USDC" + }, + { + "name": "okx", + "off_chain_ticker": "ATOM-USDC" + } + ] + }, + "ATOM/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ATOM-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "ATOMUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "ATOM-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "ATOMUSDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "ATOM_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "ATOM_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "ATOM-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "atomusdt" + }, + { + "name": "okx", + "off_chain_ticker": "ATOM-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "ATOMUSDT" + } + ] + }, + "AVAX/USD": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "avaxusd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "AVAX-USD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "AVAXUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "AVAX-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "AVAX/USD" + }, + { + "name": "okx", + "off_chain_ticker": "AVAX-USD" + } + ] + }, + "AVAX/USDC": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "AVAX-USDC" + }, + { + "name": "mexc", + "off_chain_ticker": "AVAXUSDC" + }, + { + "name": "bybit", + "off_chain_ticker": "AVAXUSDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "AVAX-USDC" + }, + { + "name": "okx", + "off_chain_ticker": "AVAX-USDC" + } + ] + }, + "AVAX/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "AVAX-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "AVAXUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "AVAX-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "AVAXUSDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "AVAX_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "AVAX_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "AVAX-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "avaxusdt" + }, + { + "name": "kraken", + "off_chain_ticker": "AVAX/USDT" + }, + { + "name": "okx", + "off_chain_ticker": "AVAX-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "AVAXUSDT" + } + ] + }, + "BCH/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "BCH-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "BCH-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "BCH/USD" + } + ] + }, + "BCH/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "BCH-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "BCHUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "BCHUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "BCH_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "bchusdt" + }, + { + "name": "okx", + "off_chain_ticker": "BCH-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "BCHUSDT" + } + ] + }, + "BLUR/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "BLUR-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "BLUR-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "BLUR/USD" + } + ] + }, + "BLUR/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "BLUR-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "BLURUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "BLUR_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "BLUR-USDT" + } + ] + }, + "BTC/USD": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "btcusd" + }, + { + "name": "coingecko", + "off_chain_ticker": "bitcoin/usd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "BTC-USD" + }, + { + "name": "bitfinex", + "off_chain_ticker": "BTCUSD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "BTCUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "BTC-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "XBT/USD" + }, + { + "name": "okx", + "off_chain_ticker": "BTC-USD" + } + ] + }, + "BTC/USDC": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "btcusdc" + }, + { + "name": "kucoin", + "off_chain_ticker": "BTC-USDC" + }, + { + "name": "mexc", + "off_chain_ticker": "BTCUSDC" + }, + { + "name": "bybit", + "off_chain_ticker": "BTCUSDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "BTC-USDC" + }, + { + "name": "huobi", + "off_chain_ticker": "btcusdc" + }, + { + "name": "kraken", + "off_chain_ticker": "XBT/USDC" + }, + { + "name": "okx", + "off_chain_ticker": "BTC-USDC" + }, + { + "name": "binance", + "off_chain_ticker": "BTCUSDC" + } + ] + }, + "BTC/USDT": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "btcusdt" + }, + { + "name": "kucoin", + "off_chain_ticker": "BTC-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "BTCUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "BTC-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "BTCUSDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "BTC_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "BTC_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "BTC-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "btcusdt" + }, + { + "name": "kraken", + "off_chain_ticker": "XBT/USDT" + }, + { + "name": "okx", + "off_chain_ticker": "BTC-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "BTCUSDT" + } + ] + }, + "COMP/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "COMP-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "COMP-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "COMP/USD" + } + ] + }, + "COMP/USDT": { + "providers": [ + { + "name": "mexc", + "off_chain_ticker": "COMPUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "COMP_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "COMP-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "COMPUSDT" + } + ] + }, + "CRV/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "CRV-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "CRV-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "CRV/USD" + } + ] + }, + "CRV/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "CRV-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "CRVUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "CRV_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "CRV-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "CRVUSDT" + } + ] + }, + "DOGE/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "DOGE-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "DOGE-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "XDG/USD" + } + ] + }, + "DOGE/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "DOGE-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "DOGEUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "DOGEUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "DOGE_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "dogeusdt" + }, + { + "name": "okx", + "off_chain_ticker": "DOGE-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "DOGEUSDT" + } + ] + }, + "DOT/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "DOT-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "DOT-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "DOT/USD" + } + ] + }, + "DOT/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "DOT-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "DOTUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "DOTUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "DOT_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "DOT-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "DOTUSDT" + } + ] + }, + "DYDX/USD": { + "providers": [ + { + "name": "coingecko", + "off_chain_ticker": "dydx-chain/usd" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "DYDXUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "DYDX-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "DYDX/USD" + }, + { + "name": "okx", + "off_chain_ticker": "DYDX-USD" + } + ] + }, + "DYDX/USDC": { + "providers": [ + { + "name": "coinbase", + "off_chain_ticker": "DYDX-USDC" + } + ] + }, + "DYDX/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "DYDX-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "DYDXUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "DYDXUSDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "DYDX_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "DYDX_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "DYDX-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "dydxusdt" + }, + { + "name": "okx", + "off_chain_ticker": "DYDX-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "DYDXUSDT" + } + ] + }, + "ETC/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "ETC-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "ETC-USD" + } + ] + }, + "ETC/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ETC-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "ETCUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "ETC_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "etcusdt" + }, + { + "name": "okx", + "off_chain_ticker": "ETC-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "ETCUSDT" + } + ] + }, + "ETH/BTC": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "ethbtc" + }, + { + "name": "coingecko", + "off_chain_ticker": "ethereum/btc" + }, + { + "name": "kucoin", + "off_chain_ticker": "ETH-BTC" + }, + { + "name": "mexc", + "off_chain_ticker": "ETHBTC" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "ETH-BTC" + }, + { + "name": "bitfinex", + "off_chain_ticker": "ETHBTC" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "ETH_BTC" + }, + { + "name": "gate.io", + "off_chain_ticker": "ETH_BTC" + }, + { + "name": "coinbase", + "off_chain_ticker": "ETH-BTC" + }, + { + "name": "huobi", + "off_chain_ticker": "ethbtc" + }, + { + "name": "kraken", + "off_chain_ticker": "ETH/XBT" + }, + { + "name": "okx", + "off_chain_ticker": "ETH-BTC" + }, + { + "name": "binance", + "off_chain_ticker": "ETHBTC" + } + ] + }, + "ETH/USD": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "ethusd" + }, + { + "name": "coingecko", + "off_chain_ticker": "ethereum/usd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "ETH-USD" + }, + { + "name": "bitfinex", + "off_chain_ticker": "ETHUSD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "ETHUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "ETH-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "ETH/USD" + }, + { + "name": "okx", + "off_chain_ticker": "ETH-USD" + } + ] + }, + "ETH/USDC": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ETH-USDC" + }, + { + "name": "mexc", + "off_chain_ticker": "ETHUSDC" + }, + { + "name": "bybit", + "off_chain_ticker": "ETHUSDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "ETH-USDC" + }, + { + "name": "huobi", + "off_chain_ticker": "ethusdc" + }, + { + "name": "kraken", + "off_chain_ticker": "ETH/USDC" + }, + { + "name": "okx", + "off_chain_ticker": "ETH-USDC" + }, + { + "name": "binance", + "off_chain_ticker": "ETHUSDC" + } + ] + }, + "ETH/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "ETH-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "ETHUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "ETH-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "ETHUSDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "ETH_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "ETH_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "ETH-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "ethusdt" + }, + { + "name": "kraken", + "off_chain_ticker": "ETH/USDT" + }, + { + "name": "okx", + "off_chain_ticker": "ETH-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "ETHUSDT" + } + ] + }, + "FIL/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "FIL-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "FIL-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "FIL/USD" + } + ] + }, + "FIL/USDT": { + "providers": [ + { + "name": "mexc", + "off_chain_ticker": "FILUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "FIL_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "filusdt" + }, + { + "name": "okx", + "off_chain_ticker": "FIL-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "FILUSDT" + } + ] + }, + "LDO/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "LDO-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "LDO-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "LDO/USD" + } + ] + }, + "LDO/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "LDO-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "LDOUSDT" + }, + { + "name": "okx", + "off_chain_ticker": "LDO-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "LDOUSDT" + } + ] + }, + "LINK/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "LINK-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "LINK-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "LINK/USD" + } + ] + }, + "LINK/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "LINK-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "LINKUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "LINKUSDT" + }, + { + "name": "okx", + "off_chain_ticker": "LINK-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "LINKUSDT" + } + ] + }, + "LTC/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "LTC-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "LTC-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "XLTCZ/USD" + } + ] + }, + "LTC/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "LTC-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "LTCUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "LTCUSDT" + }, + { + "name": "huobi", + "off_chain_ticker": "ltcusdt" + }, + { + "name": "okx", + "off_chain_ticker": "LTC-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "LTCUSDT" + } + ] + }, + "MATIC/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "MATIC-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "MATIC-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "MATIC/USD" + } + ] + }, + "MATIC/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "MATIC-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "MATICUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "MATICUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "MATIC_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "maticusdt" + }, + { + "name": "okx", + "off_chain_ticker": "MATIC-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "MATICUSDT" + } + ] + }, + "MKR/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "MKR-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "MKR-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "MKR/USD" + } + ] + }, + "MKR/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "MKR-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "MKRUSDT" + }, + { + "name": "okx", + "off_chain_ticker": "MKR-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "MKRUSDT" + } + ] + }, + "NEAR/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "NEAR-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "NEAR-USD" + } + ] + }, + "NEAR/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "NEAR-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "NEARUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "NEAR_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "nearusdt" + }, + { + "name": "okx", + "off_chain_ticker": "NEAR-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "NEARUSDT" + } + ] + }, + "OP/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "OP-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "OP-USD" + } + ] + }, + "OP/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "OP-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "OPUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "OP_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "OP-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "OPUSDT" + } + ] + }, + "OSMO/USD": { + "providers": [ + { + "name": "coingecko", + "off_chain_ticker": "osmosis/usd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "OSMO-USD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "OSMO_USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "OSMO-USD" + } + ] + }, + "OSMO/USDC": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "OSMO-USDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "OSMO-USDC" + } + ] + }, + "OSMO/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "OSMO-USDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "OSMO-USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "OSMO-USDT" + } + ] + }, + "PEPE/USD": { + "providers": [ + { + "name": "kraken", + "off_chain_ticker": "PEPE/USD" + } + ] + }, + "PEPE/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "PEPE-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "PEPEUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "PEPEUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "PEPE_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "PEPE-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "PEPEUSDT" + } + ] + }, + "SEI/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "SEI-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "SEI-USD" + } + ] + }, + "SEI/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "SEI-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "SEIUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "SEIUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "SEI_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "seiusdt" + }, + { + "name": "binance", + "off_chain_ticker": "SEIUSDT" + } + ] + }, + "SHIB/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "SHIB-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "SHIB-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "SHIB/USD" + } + ] + }, + "SHIB/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "SHIB-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "SHIBUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "SHIBUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "SHIB_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "SHIB-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "SHIBUSDT" + } + ] + }, + "SOL/USD": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "solusd" + }, + { + "name": "coingecko", + "off_chain_ticker": "solana/usd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "SOL-USD" + }, + { + "name": "bitfinex", + "off_chain_ticker": "SOLUSD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "SOLUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "SOL-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "SOL/USD" + }, + { + "name": "okx", + "off_chain_ticker": "SOL-USD" + } + ] + }, + "SOL/USDC": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "SOL-USDC" + }, + { + "name": "mexc", + "off_chain_ticker": "SOLUSDC" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "SOL-USDC" + }, + { + "name": "bybit", + "off_chain_ticker": "SOLUSDC" + }, + { + "name": "gate.io", + "off_chain_ticker": "SOL_USDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "SOL-USDC" + }, + { + "name": "okx", + "off_chain_ticker": "SOL-USDC" + }, + { + "name": "binance", + "off_chain_ticker": "SOLUSDC" + } + ] + }, + "SOL/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "SOL-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "SOLUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "SOL-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "SOLUSDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "SOL_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "SOL_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "SOL-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "solusdt" + }, + { + "name": "kraken", + "off_chain_ticker": "SOL/USDT" + }, + { + "name": "okx", + "off_chain_ticker": "SOL-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "SOLUSDT" + } + ] + }, + "SUI/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "SUI-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "SUI-USD" + } + ] + }, + "SUI/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "SUI-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "SUIUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "SUIUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "SUI_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "suiusdt" + }, + { + "name": "okx", + "off_chain_ticker": "SUI-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "SUIUSDT" + } + ] + }, + "TIA/USD": { + "providers": [ + { + "name": "coingecko", + "off_chain_ticker": "celestia/usd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "TIA-USD" + }, + { + "name": "bitfinex", + "off_chain_ticker": "TIAUSD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "TIAUSD-PERP" + }, + { + "name": "coinbase", + "off_chain_ticker": "TIA-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "TIA/USD" + }, + { + "name": "okx", + "off_chain_ticker": "TIA-USD" + } + ] + }, + "TIA/USDC": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "TIA-USDC" + }, + { + "name": "coinbase", + "off_chain_ticker": "TIA-USDC" + } + ] + }, + "TIA/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "TIA-USDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "TIA-USDT" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "TIA_USDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "TIA_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "TIA-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "tiausdt" + }, + { + "name": "okx", + "off_chain_ticker": "TIA-USDT" + } + ] + }, + "TRX/USD": { + "providers": [ + { + "name": "kraken", + "off_chain_ticker": "TRX/USD" + } + ] + }, + "TRX/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "TRX-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "TRXUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "TRXUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "TRX_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "trxusdt" + }, + { + "name": "okx", + "off_chain_ticker": "TRX-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "TRXUSDT" + } + ] + }, + "UNI/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "UNI-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "UNI-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "UNI/USD" + } + ] + }, + "UNI/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "UNI-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "UNIUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "UNI_USDT" + }, + { + "name": "okx", + "off_chain_ticker": "UNI-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "UNIUSDT" + } + ] + }, + "USDC/USD": { + "providers": [ + { + "name": "coinbase", + "off_chain_ticker": "USDC-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "USDC/USD" + }, + { + "name": "okx", + "off_chain_ticker": "USDC-USD" + } + ] + }, + "USDC/USDT": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "usdcusdt" + }, + { + "name": "kucoin", + "off_chain_ticker": "USDC-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "USDCUSDT" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "USDC-USDT" + }, + { + "name": "bybit", + "off_chain_ticker": "USDCUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "USDC_USDT" + }, + { + "name": "coinbase", + "off_chain_ticker": "USDC-USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "usdcusdt" + }, + { + "name": "kraken", + "off_chain_ticker": "USDC/USDT" + }, + { + "name": "okx", + "off_chain_ticker": "USDC-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "USDCUSDT" + } + ] + }, + "USDT/USD": { + "providers": [ + { + "name": "bitstamp", + "off_chain_ticker": "usdtusd" + }, + { + "name": "coinbase_websocket", + "off_chain_ticker": "USDT-USD" + }, + { + "name": "crypto_dot_com", + "off_chain_ticker": "USDT_USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "USDT-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "USDT/USD" + }, + { + "name": "okx", + "off_chain_ticker": "USDT-USD" + } + ] + }, + "WLD/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "WLD-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "WLDUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "WLDUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "WLD_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "wldusdt" + }, + { + "name": "okx", + "off_chain_ticker": "WLD-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "WLDUSDT" + } + ] + }, + "XLM/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "XLM-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "XLM-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "XXLMZ/USD" + } + ] + }, + "XLM/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "XLM-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "XLMUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "XLMUSDT" + }, + { + "name": "okx", + "off_chain_ticker": "XLM-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "XLMUSDT" + } + ] + }, + "XRP/USD": { + "providers": [ + { + "name": "coinbase_websocket", + "off_chain_ticker": "XRP-USD" + }, + { + "name": "coinbase", + "off_chain_ticker": "XRP-USD" + }, + { + "name": "kraken", + "off_chain_ticker": "XXRPZ/USD" + } + ] + }, + "XRP/USDT": { + "providers": [ + { + "name": "kucoin", + "off_chain_ticker": "XRP-USDT" + }, + { + "name": "mexc", + "off_chain_ticker": "XRPUSDT" + }, + { + "name": "bybit", + "off_chain_ticker": "XRPUSDT" + }, + { + "name": "gate.io", + "off_chain_ticker": "XRP_USDT" + }, + { + "name": "huobi", + "off_chain_ticker": "xrpusdt" + }, + { + "name": "okx", + "off_chain_ticker": "XRP-USDT" + }, + { + "name": "binance", + "off_chain_ticker": "XRPUSDT" + } + ] + } + } +} diff --git a/protocol/daemons/slinky/config/oracle.json b/protocol/daemons/slinky/config/oracle.json new file mode 100644 index 0000000000..bbcf0d4d48 --- /dev/null +++ b/protocol/daemons/slinky/config/oracle.json @@ -0,0 +1,403 @@ +{ + "updateInterval": 1500000000, + "maxPriceAge": 120000000000, + "providers": [ + { + "name": "binance", + "api": { + "enabled": true, + "timeout": 500000000, + "interval": 150000000, + "maxQueries": 1, + "atomic": true, + "url": "https://api.binance.com/api/v3/ticker/price?symbols=%s%s%s", + "name": "binance" + }, + "webSocket": { + "enabled": false, + "maxBufferSize": 0, + "reconnectionTimeout": 0, + "wss": "", + "name": "", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 0, + "enableCompression": false, + "readTimeout": 0, + "writeTimeout": 0, + "pingInterval": 0, + "maxReadErrorCount": 0, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "coinbase", + "api": { + "enabled": true, + "timeout": 500000000, + "interval": 20000000, + "maxQueries": 10, + "atomic": false, + "url": "https://api.coinbase.com/v2/prices/%s/spot", + "name": "coinbase" + }, + "webSocket": { + "enabled": false, + "maxBufferSize": 0, + "reconnectionTimeout": 0, + "wss": "", + "name": "", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 0, + "enableCompression": false, + "readTimeout": 0, + "writeTimeout": 0, + "pingInterval": 0, + "maxReadErrorCount": 0, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "coingecko", + "api": { + "enabled": true, + "timeout": 500000000, + "interval": 15000000000, + "maxQueries": 1, + "atomic": true, + "url": "https://api.coingecko.com/api/v3", + "name": "coingecko" + }, + "webSocket": { + "enabled": false, + "maxBufferSize": 0, + "reconnectionTimeout": 0, + "wss": "", + "name": "", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 0, + "enableCompression": false, + "readTimeout": 0, + "writeTimeout": 0, + "pingInterval": 0, + "maxReadErrorCount": 0, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "bitfinex", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://api-pub.bitfinex.com/ws/2", + "name": "bitfinex", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "bitstamp", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1024, + "reconnectionTimeout": 10000000000, + "wss": "wss://ws.bitstamp.net", + "name": "bitstamp", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 10000000000, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "bybit", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://stream.bybit.com/v5/public/spot", + "name": "bybit", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 15000000000, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "coinbase_websocket", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1024, + "reconnectionTimeout": 10000000000, + "wss": "wss://ws-feed.exchange.coinbase.com", + "name": "coinbase_websocket", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 5000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "crypto_dot_com", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1024, + "reconnectionTimeout": 10000000000, + "wss": "wss://stream.crypto.com/exchange/v1/market", + "name": "crypto_dot_com", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "gate.io", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://api.gateio.ws/ws/v4/", + "name": "gate.io", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "huobi", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://api.huobi.pro/ws", + "name": "huobi", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "kraken", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://ws.kraken.com", + "name": "kraken", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "kucoin", + "api": { + "enabled": false, + "timeout": 5000000000, + "interval": 60000000000, + "maxQueries": 1, + "atomic": false, + "url": "https://api.kucoin.com", + "name": "kucoin" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1024, + "reconnectionTimeout": 10000000000, + "wss": "wss://ws-api-spot.kucoin.com/", + "name": "kucoin", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 10000000000, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "mexc", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://wbs.mexc.com/ws", + "name": "mexc", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 20000000000, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + }, + { + "name": "okx", + "api": { + "enabled": false, + "timeout": 0, + "interval": 0, + "maxQueries": 0, + "atomic": false, + "url": "", + "name": "" + }, + "webSocket": { + "enabled": true, + "maxBufferSize": 1000, + "reconnectionTimeout": 10000000000, + "wss": "wss://ws.okx.com:8443/ws/v5/public", + "name": "okx", + "readBufferSize": 0, + "writeBufferSize": 0, + "handshakeTimeout": 45000000000, + "enableCompression": false, + "readTimeout": 45000000000, + "writeTimeout": 45000000000, + "pingInterval": 0, + "maxReadErrorCount": 100, + "maxSubscriptionsPerConnection": 0 + } + } + ], + "production": false, + "metrics": { + "prometheusServerAddress": "0.0.0.0:8002", + "enabled": true + } +} diff --git a/protocol/docker-compose.yml b/protocol/docker-compose.yml index 04d3a2fb23..d5a2b384ff 100644 --- a/protocol/docker-compose.yml +++ b/protocol/docker-compose.yml @@ -112,6 +112,20 @@ services: - DAEMON_HOME=/dydxprotocol/chain/.dave volumes: - ./localnet/dydxprotocol3:/dydxprotocol/chain/.dave/data + slinky0: + image: local:dydxprotocol + entrypoint: + - slinky + - -oracle-config-path + - /etc/oracle.json + - -market-config-path + - /etc/market.json + - -host + - "0.0.0.0" + - -port + - "8080" + ports: + - "8080:8080" datadog-agent: image: public.ecr.aws/datadog/agent:7 diff --git a/protocol/go.mod b/protocol/go.mod index 973671e84d..5f093096bb 100644 --- a/protocol/go.mod +++ b/protocol/go.mod @@ -1,6 +1,8 @@ module github.com/dydxprotocol/v4-chain/protocol -go 1.21 +go 1.22 + +toolchain go1.22.0 require ( cosmossdk.io/api v0.7.3 @@ -17,7 +19,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.3 - github.com/golangci/golangci-lint v1.55.1 + github.com/golangci/golangci-lint v1.55.2 github.com/google/go-cmp v0.6.0 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/grpc-gateway v1.16.0 @@ -29,11 +31,11 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - github.com/vektra/mockery/v2 v2.23.1 + github.com/vektra/mockery/v2 v2.40.1 github.com/zyedidia/generic v1.0.0 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/grpc v1.60.1 + google.golang.org/grpc v1.61.0 gopkg.in/DataDog/dd-trace-go.v1 v1.48.0 gopkg.in/typ.v4 v4.1.0 ) @@ -44,12 +46,12 @@ require ( cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.3.1 cosmossdk.io/store v1.0.2 - cosmossdk.io/tools/confix v0.1.0 + cosmossdk.io/tools/confix v0.1.1 cosmossdk.io/x/circuit v0.1.0 cosmossdk.io/x/evidence v0.1.0 cosmossdk.io/x/feegrant v0.1.0 cosmossdk.io/x/tx v0.13.0 - cosmossdk.io/x/upgrade v0.1.0 + cosmossdk.io/x/upgrade v0.1.1 github.com/burdiyan/kafkautil v0.0.0-20190131162249-eaf83ed22d5b github.com/cosmos/cosmos-db v1.0.0 github.com/cosmos/iavl v1.0.1 @@ -64,9 +66,10 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/rs/zerolog v1.32.0 github.com/shopspring/decimal v1.3.1 + github.com/skip-mev/slinky v0.2.2 github.com/spf13/viper v1.18.2 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d - google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f + google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 google.golang.org/protobuf v1.32.0 ) @@ -113,14 +116,14 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect - github.com/bits-and-blooms/bitset v1.8.0 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/bkielbasa/cyclop v1.2.1 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v3 v3.4.0 // indirect github.com/breml/bidichk v0.2.7 // indirect github.com/breml/errchkjson v0.3.6 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect - github.com/butuzov/ireturn v0.2.1 // indirect + github.com/butuzov/ireturn v0.2.2 // indirect github.com/butuzov/mirror v1.1.0 // indirect github.com/catenacyber/perfsprint v0.2.0 // indirect github.com/ccojocar/zxcvbn-go v1.0.1 // indirect @@ -130,7 +133,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect - github.com/chigopher/pathlib v0.12.0 // indirect + github.com/chigopher/pathlib v0.19.1 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect github.com/cockroachdb/errors v1.11.1 // indirect @@ -228,7 +231,7 @@ require ( github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-getter v1.7.1 // indirect + github.com/hashicorp/go-getter v1.7.3 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -243,6 +246,7 @@ require ( github.com/hexops/gotextdiff v1.0.3 // indirect github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c // indirect github.com/huandu/skiplist v1.2.0 // indirect + github.com/huandu/xstrings v1.4.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect @@ -254,7 +258,7 @@ require ( github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jgautheron/goconst v1.6.0 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jinzhu/copier v0.3.5 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect @@ -285,7 +289,7 @@ require ( github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mbilski/exhaustivestruct v1.2.0 // indirect github.com/mgechev/revive v1.3.4 // indirect github.com/minio/highwayhash v1.0.2 // indirect @@ -298,7 +302,7 @@ require ( github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.11.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.14.0 // indirect + github.com/nunnatsa/ginkgolinter v0.14.1 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.1.0 // indirect @@ -308,7 +312,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/opencontainers/runc v1.1.5 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -348,7 +352,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect @@ -379,10 +383,8 @@ require ( go.etcd.io/bbolt v1.3.8 // indirect go.opencensus.io v0.24.0 // indirect go.tmz.dev/musttag v0.7.2 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/goleak v1.1.12 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect golang.org/x/mod v0.15.0 // indirect @@ -405,7 +407,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect honnef.co/go/tools v0.4.6 // indirect - mvdan.cc/gofumpt v0.5.0 // indirect + mvdan.cc/gofumpt v0.6.0 // indirect mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect diff --git a/protocol/go.sum b/protocol/go.sum index 9209ae8845..91cafa0063 100644 --- a/protocol/go.sum +++ b/protocol/go.sum @@ -202,8 +202,8 @@ cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= cosmossdk.io/math v1.2.0 h1:8gudhTkkD3NxOP2YyyJIYYmt6dQ55ZfJkDOaxXpy7Ig= cosmossdk.io/math v1.2.0/go.mod h1:l2Gnda87F0su8a/7FEKJfFdJrM0JZRXQaohlgJeyQh0= -cosmossdk.io/tools/confix v0.1.0 h1:2OOZTtQsDT5e7P3FM5xqM0bPfluAxZlAwxqaDmYBE+E= -cosmossdk.io/tools/confix v0.1.0/go.mod h1:TdXKVYs4gEayav5wM+JHT+kTU2J7fozFNqoVaN+8CdY= +cosmossdk.io/tools/confix v0.1.1 h1:aexyRv9+y15veH3Qw16lxQwo+ki7r2I+g0yNTEFEQM8= +cosmossdk.io/tools/confix v0.1.1/go.mod h1:nQVvP1tHsGXS83PonPVWJtSbddIqyjEw99L4M3rPJyQ= cosmossdk.io/x/circuit v0.1.0 h1:IAej8aRYeuOMritczqTlljbUVHq1E85CpBqaCTwYgXs= cosmossdk.io/x/circuit v0.1.0/go.mod h1:YDzblVE8+E+urPYQq5kq5foRY/IzhXovSYXb4nwd39w= cosmossdk.io/x/evidence v0.1.0 h1:J6OEyDl1rbykksdGynzPKG5R/zm6TacwW2fbLTW4nCk= @@ -212,8 +212,8 @@ cosmossdk.io/x/feegrant v0.1.0 h1:c7s3oAq/8/UO0EiN1H5BIjwVntujVTkYs35YPvvrdQk= cosmossdk.io/x/feegrant v0.1.0/go.mod h1:4r+FsViJRpcZif/yhTn+E0E6OFfg4n0Lx+6cCtnZElU= cosmossdk.io/x/tx v0.13.0 h1:8lzyOh3zONPpZv2uTcUmsv0WTXy6T1/aCVDCqShmpzU= cosmossdk.io/x/tx v0.13.0/go.mod h1:CpNQtmoqbXa33/DVxWQNx5Dcnbkv2xGUhL7tYQ5wUsY= -cosmossdk.io/x/upgrade v0.1.0 h1:z1ZZG4UL9ICTNbJDYZ6jOnF9GdEK9wyoEFi4BUScHXE= -cosmossdk.io/x/upgrade v0.1.0/go.mod h1:/6jjNGbiPCNtmA1N+rBtP601sr0g4ZXuj3yC6ClPCGY= +cosmossdk.io/x/upgrade v0.1.1 h1:aoPe2gNvH+Gwt/Pgq3dOxxQVU3j5P6Xf+DaUJTDZATc= +cosmossdk.io/x/upgrade v0.1.1/go.mod h1:MNLptLPcIFK9CWt7Ra//8WUZAxweyRDNcbs5nkOcQy0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -286,12 +286,12 @@ github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/ github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I= github.com/adlio/schema v1.3.3/go.mod h1:1EsRssiv9/Ce2CMzq5DoL7RiMshhuigQxrR4DMV9fHg= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= -github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w= +github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= github.com/alecthomas/go-check-sumtype v0.1.3 h1:M+tqMxB68hcgccRXBMVCPI4UJ+QUfdSx0xdbypKCqA8= github.com/alecthomas/go-check-sumtype v0.1.3/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= +github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -323,7 +323,6 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.44.224 h1:09CiaaF35nRmxrzWZ2uRq5v6Ghg/d2RiPjZnSgtt+RQ= github.com/aws/aws-sdk-go v1.44.224/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -334,8 +333,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c= -github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= @@ -356,8 +355,8 @@ github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28 github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/burdiyan/kafkautil v0.0.0-20190131162249-eaf83ed22d5b h1:gRFujk0F/KYFDEalhpaAbLIwmeiDH53ZgdllJ7UHxyQ= github.com/burdiyan/kafkautil v0.0.0-20190131162249-eaf83ed22d5b/go.mod h1:5hrpM9I1h0fZlTk8JhqaaBaCs76EbCGvFcPtm5SxcCU= -github.com/butuzov/ireturn v0.2.1 h1:w5Ks4tnfeFDZskGJ2x1GAkx5gaQV+kdU3NKNr3NEBzY= -github.com/butuzov/ireturn v0.2.1/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= +github.com/butuzov/ireturn v0.2.2 h1:jWI36dxXwVrI+RnXDwux2IZOewpmfv930OuIRfaBUJ0= +github.com/butuzov/ireturn v0.2.2/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -387,8 +386,8 @@ github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAc github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chigopher/pathlib v0.12.0 h1:1GM7fN/IwXXmOHbd1jkMqHD2wUhYqUvafgxTwmLT/q8= -github.com/chigopher/pathlib v0.12.0/go.mod h1:EJ5UtJ/sK8Nt6q3VWN+EwZLZ3g0afJiG8NegYiQQ/gQ= +github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGznx5A= +github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -738,8 +737,8 @@ github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6 github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/golangci-lint v1.55.1 h1:DL2j9Eeapg1N3WEkKnQFX5L40SYtjZZJjGVdyEgNrDc= -github.com/golangci/golangci-lint v1.55.1/go.mod h1:z00biPRqjo5MISKV1+RWgONf2KvrPDmfqxHpHKB6bI4= +github.com/golangci/golangci-lint v1.55.2 h1:yllEIsSJ7MtlDBwDJ9IMBkyEUz2fYE0b5B8IUgO1oP8= +github.com/golangci/golangci-lint v1.55.2/go.mod h1:H60CZ0fuqoTwlTvnbyjhpZPWp7KmsjwV2yupIMiMXbM= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= @@ -883,8 +882,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.7.1 h1:SWiSWN/42qdpR0MdhaOc/bLR48PLuP1ZQtYLRlM69uY= -github.com/hashicorp/go-getter v1.7.1/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-getter v1.7.3 h1:bN2+Fw9XPFvOCjB0UOevFIMICZ7G2XSQHzfvLUyOM5E= +github.com/hashicorp/go-getter v1.7.3/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -939,6 +938,8 @@ github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3 github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= @@ -974,8 +975,8 @@ github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFo github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -1022,7 +1023,6 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -1094,8 +1094,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo= github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= @@ -1152,8 +1152,8 @@ github.com/nishanths/exhaustive v0.11.0 h1:T3I8nUGhl/Cwu5Z2hfc92l0e04D2GEW6e0l8p github.com/nishanths/exhaustive v0.11.0/go.mod h1:RqwDsZ1xY0dNdqHho2z6X+bgzizwbLYOWnZbbl2wLB4= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.14.0 h1:XQPNmw+kZz5cC/HbFK3mQutpjzAQv1dHregRA+4CGGg= -github.com/nunnatsa/ginkgolinter v0.14.0/go.mod h1:cm2xaqCUCRd7qcP4DqbVvpcyEMkuLM9CF0wY6VASohk= +github.com/nunnatsa/ginkgolinter v0.14.1 h1:khx0CqR5U4ghsscjJ+lZVthp3zjIFytRXPTaQ/TMiyA= +github.com/nunnatsa/ginkgolinter v0.14.1/go.mod h1:nY0pafUSst7v7F637e7fymaMlQqI9c0Wka2fGsDkzWg= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -1216,8 +1216,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc h1:8bQZVK1X6BJR/6nYUPxQEP+ReTsceJTKizeuwjWOPUA= @@ -1236,7 +1236,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1296,7 +1295,6 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -1362,6 +1360,8 @@ github.com/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= +github.com/skip-mev/slinky v0.2.2 h1:LlJDfmr45l3G+3GJhNRyWS72+FKAWjQxDfhZkzOC0/w= +github.com/skip-mev/slinky v0.2.2/go.mod h1:HBzskXU/8d7cNthgmksHp+KZHQ7vZbGazfsnT2SqQhI= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1376,7 +1376,6 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -1406,8 +1405,9 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1476,8 +1476,8 @@ github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa h1:5SqCsI/2Qya2bC github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= -github.com/vektra/mockery/v2 v2.23.1 h1:N59FENM2d/gWE6Ns5JPuf9a7jqQWeheGefZqvuvb1dM= -github.com/vektra/mockery/v2 v2.23.1/go.mod h1:Zh3Kv1ckKs6FokhlVLcCu6UTyzfS3M8mpROz1lBNp+w= +github.com/vektra/mockery/v2 v2.40.1 h1:8D01rBqloDLDHKZGXkyUD9Yj5Z+oDXBqDZ+tRXYM/oA= +github.com/vektra/mockery/v2 v2.40.1/go.mod h1:dPzGtjT0/Uu4hqpF6QNHwz+GLago7lq1bxdj9wHbGKo= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -1539,11 +1539,9 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -1553,8 +1551,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= @@ -1568,7 +1566,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -2154,8 +2151,8 @@ google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= -google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= -google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o= +google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2199,8 +2196,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2277,8 +2274,8 @@ honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU= inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k= -mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= -mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= +mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= diff --git a/protocol/lib/slinky/utils.go b/protocol/lib/slinky/utils.go new file mode 100644 index 0000000000..5b5c6a5eff --- /dev/null +++ b/protocol/lib/slinky/utils.go @@ -0,0 +1,29 @@ +package slinky + +import ( + "fmt" + "strings" + + "github.com/skip-mev/slinky/x/oracle/types" +) + +/* + * Slinky utility functions + * + * This file contains functions for converting between x/prices types and slinky's x/oracle equivalents. + */ + +// MarketPairToCurrencyPair converts a base and quote pair from MarketPrice format (for example BTC-ETH) +// to a currency pair type. Returns an error if unable to convert. +func MarketPairToCurrencyPair(marketPair string) (types.CurrencyPair, error) { + split := strings.Split(marketPair, "-") + if len(split) != 2 { + return types.CurrencyPair{}, fmt.Errorf("incorrectly formatted CurrencyPair: %s", marketPair) + } + cp := types.CurrencyPair{ + Base: strings.ToUpper(split[0]), + Quote: strings.ToUpper(split[1]), + } + + return cp, cp.ValidateBasic() +} diff --git a/protocol/lib/slinky/utils_test.go b/protocol/lib/slinky/utils_test.go new file mode 100644 index 0000000000..7756cacc39 --- /dev/null +++ b/protocol/lib/slinky/utils_test.go @@ -0,0 +1,34 @@ +package slinky_test + +import ( + "fmt" + "testing" + + "github.com/skip-mev/slinky/x/oracle/types" + "github.com/stretchr/testify/require" + + "github.com/dydxprotocol/v4-chain/protocol/lib/slinky" +) + +func TestMarketPairToCurrencyPair(t *testing.T) { + testCases := []struct { + mp string + cp types.CurrencyPair + err error + }{ + {mp: "FOO-BAR", cp: types.CurrencyPair{Base: "FOO", Quote: "BAR"}, err: nil}, + {mp: "FOOBAR", cp: types.CurrencyPair{}, err: fmt.Errorf("incorrectly formatted CurrencyPair: FOOBAR")}, + {mp: "FOO/BAR", cp: types.CurrencyPair{}, err: fmt.Errorf("incorrectly formatted CurrencyPair: FOOBAR")}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("TestMarketPair %s", tc.mp), func(t *testing.T) { + cp, err := slinky.MarketPairToCurrencyPair(tc.mp) + if tc.err != nil { + require.Error(t, err, tc.err) + } else { + require.NoError(t, err) + require.Equal(t, tc.cp, cp) + } + }) + } +} diff --git a/protocol/mocks/Makefile b/protocol/mocks/Makefile index bad625d00b..29f4641b29 100644 --- a/protocol/mocks/Makefile +++ b/protocol/mocks/Makefile @@ -55,3 +55,6 @@ mock-gen: @go run github.com/vektra/mockery/v2 --name=BridgeServiceClient --dir=./daemons/bridge/api --recursive --output=./mocks @go run github.com/vektra/mockery/v2 --name=BridgeQueryClient --dir=./daemons/bridge/client/types --recursive --output=./mocks @go run github.com/vektra/mockery/v2 --name=EthClient --dir=./daemons/bridge/client/types --recursive --output=./mocks + @go run github.com/vektra/mockery/v2 --name=PriceFetcher --dir=./daemons/slinky/client --recursive --output=./mocks + @go run github.com/vektra/mockery/v2 --name=MarketPairFetcher --dir=./daemons/slinky/client --recursive --output=./mocks + @go run github.com/vektra/mockery/v2 --name=OracleClient --dir=$(GOPATH)/pkg/mod/github.com/skip-mev/slinky@$(SLINKY_VERSION)/service/clients/oracle --filename=oracle_client.go --recursive --output=./mocks diff --git a/protocol/mocks/MarketPairFetcher.go b/protocol/mocks/MarketPairFetcher.go new file mode 100644 index 0000000000..9e1a43ce2d --- /dev/null +++ b/protocol/mocks/MarketPairFetcher.go @@ -0,0 +1,102 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + flags "github.com/dydxprotocol/v4-chain/protocol/app/flags" + daemonstypes "github.com/dydxprotocol/v4-chain/protocol/daemons/types" + + mock "github.com/stretchr/testify/mock" + + types "github.com/skip-mev/slinky/x/oracle/types" +) + +// MarketPairFetcher is an autogenerated mock type for the MarketPairFetcher type +type MarketPairFetcher struct { + mock.Mock +} + +// FetchIdMappings provides a mock function with given fields: _a0 +func (_m *MarketPairFetcher) FetchIdMappings(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for FetchIdMappings") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetIDForPair provides a mock function with given fields: _a0 +func (_m *MarketPairFetcher) GetIDForPair(_a0 types.CurrencyPair) (uint32, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetIDForPair") + } + + var r0 uint32 + var r1 error + if rf, ok := ret.Get(0).(func(types.CurrencyPair) (uint32, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(types.CurrencyPair) uint32); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint32) + } + + if rf, ok := ret.Get(1).(func(types.CurrencyPair) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Start provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MarketPairFetcher) Start(_a0 context.Context, _a1 flags.Flags, _a2 daemonstypes.GrpcClient) error { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, flags.Flags, daemonstypes.GrpcClient) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stop provides a mock function with given fields: +func (_m *MarketPairFetcher) Stop() { + _m.Called() +} + +// NewMarketPairFetcher creates a new instance of MarketPairFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMarketPairFetcher(t interface { + mock.TestingT + Cleanup(func()) +}) *MarketPairFetcher { + mock := &MarketPairFetcher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/protocol/mocks/OracleClient.go b/protocol/mocks/OracleClient.go new file mode 100644 index 0000000000..9f6ce188cc --- /dev/null +++ b/protocol/mocks/OracleClient.go @@ -0,0 +1,105 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + types "github.com/skip-mev/slinky/service/servers/oracle/types" +) + +// OracleClient is an autogenerated mock type for the OracleClient type +type OracleClient struct { + mock.Mock +} + +// Prices provides a mock function with given fields: ctx, in, opts +func (_m *OracleClient) Prices(ctx context.Context, in *types.QueryPricesRequest, opts ...grpc.CallOption) (*types.QueryPricesResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Prices") + } + + var r0 *types.QueryPricesResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryPricesRequest, ...grpc.CallOption) (*types.QueryPricesResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryPricesRequest, ...grpc.CallOption) *types.QueryPricesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryPricesResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryPricesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Start provides a mock function with given fields: _a0 +func (_m *OracleClient) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stop provides a mock function with given fields: +func (_m *OracleClient) Stop() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Stop") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewOracleClient creates a new instance of OracleClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOracleClient(t interface { + mock.TestingT + Cleanup(func()) +}) *OracleClient { + mock := &OracleClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/protocol/mocks/PriceFetcher.go b/protocol/mocks/PriceFetcher.go new file mode 100644 index 0000000000..c2e798e796 --- /dev/null +++ b/protocol/mocks/PriceFetcher.go @@ -0,0 +1,69 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// PriceFetcher is an autogenerated mock type for the PriceFetcher type +type PriceFetcher struct { + mock.Mock +} + +// FetchPrices provides a mock function with given fields: ctx +func (_m *PriceFetcher) FetchPrices(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for FetchPrices") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: ctx +func (_m *PriceFetcher) Start(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Stop provides a mock function with given fields: +func (_m *PriceFetcher) Stop() { + _m.Called() +} + +// NewPriceFetcher creates a new instance of PriceFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPriceFetcher(t interface { + mock.TestingT + Cleanup(func()) +}) *PriceFetcher { + mock := &PriceFetcher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/protocol/scripts/genesis/sample_pregenesis.json b/protocol/scripts/genesis/sample_pregenesis.json index ac04970e33..5cc71f0073 100644 --- a/protocol/scripts/genesis/sample_pregenesis.json +++ b/protocol/scripts/genesis/sample_pregenesis.json @@ -1,4 +1,5 @@ { + "app_hash": null, "app_name": "dydxprotocold", "app_state": { "assets": { @@ -524,6 +525,7 @@ } } }, + "consensus": null, "crisis": { "constant_fee": { "amount": "1000000000000000000", @@ -758,6 +760,7 @@ }, "gov": { "constitution": "", + "deposit_params": null, "deposits": [], "params": { "burn_proposal_deposit_prevote": false, @@ -789,7 +792,9 @@ }, "proposals": [], "starting_proposal_id": "1", - "votes": [] + "tally_params": null, + "votes": [], + "voting_params": null }, "govplus": {}, "ibc": { @@ -854,6 +859,7 @@ "port": "icahost" } }, + "params": null, "perpetuals": { "liquidity_tiers": [ { @@ -1824,7 +1830,7 @@ ] } }, - "app_version": "4.0.0-dev0-62-g95853be5", + "app_version": "4.0.0-dev0-101-g18028fd0", "chain_id": "dydx-sample-1", "consensus": { "params": { diff --git a/protocol/testing/testnet-local/local.sh b/protocol/testing/testnet-local/local.sh index edb7112c18..6898682118 100755 --- a/protocol/testing/testnet-local/local.sh +++ b/protocol/testing/testnet-local/local.sh @@ -95,6 +95,7 @@ create_validators() { cat <<<"$new_file" >"$VAL_CONFIG_DIR"/node_key.json edit_config "$VAL_CONFIG_DIR" + use_slinky "$VAL_CONFIG_DIR" # Using "*" as a subscript results in a single arg: "dydx1... dydx1... dydx1..." # Using "@" as a subscript results in separate args: "dydx1..." "dydx1..." "dydx1..." @@ -153,6 +154,15 @@ setup_cosmovisor() { done } +use_slinky() { + CONFIG_FOLDER=$1 + # Disable pricefeed-daemon + dasel put -t bool -f "$CONFIG_FOLDER"/app.toml 'price-daemon-enabled' -v false + # Enable slinky daemon + dasel put -t bool -f "$CONFIG_FOLDER"/app.toml 'slinky-daemon-enabled' -v true + dasel put -t string -f "$VAL_CONFIG_DIR"/app.toml '.oracle.oracle_address' -v 'slinky0:8080' +} + # TODO(DEC-1894): remove this function once we migrate off of persistent peers. # Note: DO NOT add more config modifications in this method. Use `cmd/config.go` to configure # the default config values. diff --git a/protocol/x/prices/keeper/market_price_test.go b/protocol/x/prices/keeper/market_price_test.go index d82422948e..c41be0114b 100644 --- a/protocol/x/prices/keeper/market_price_test.go +++ b/protocol/x/prices/keeper/market_price_test.go @@ -160,9 +160,8 @@ func TestGetMarketIdToValidIndexPrice(t *testing.T) { marketIdToIndexPrice := keeper.GetMarketIdToValidIndexPrice(ctx) // While there are 4 markets in state, only 7, 8, 9 have index prices, // and only 8, 9 have valid median index prices. - // Market7 only has 1 valid price due to update time constraint, - // but the min exchanges required is 2. Therefore, no median price. - require.Len(t, marketIdToIndexPrice, 2) + // Market7 only has 1 valid price due to update time constraint. + require.Len(t, marketIdToIndexPrice, 3) require.Equal(t, types.MarketPrice{ Id: constants.MarketId9,