From e63289c88df34a497690eadf1985a99e9959b787 Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 17 Jan 2024 06:37:45 -0500 Subject: [PATCH 01/10] pricefeed setup --- app/app.go | 83 +- app/default_overrides.go | 122 +- app/flags/flags.go | 136 ++ cmd/layerd/cmd/init.go | 29 + cmd/layerd/cmd/price.go | 37 + cmd/layerd/cmd/root.go | 15 +- cmd/layerd/main.go | 4 +- daemons/configs/default_config.go | 123 ++ daemons/configs/default_config_test.go | 247 +++ .../configs/default_market_param_config.go | 118 ++ daemons/configs/test_data/broken_test.toml | 6 + .../configs/test_data/missingvals_test.toml | 4 + daemons/configs/test_data/valid_test.toml | 5 + .../configs/test_data/wrongvaltype_test.toml | 6 + daemons/constants/ipc.go | 6 + daemons/constants/logger.go | 35 + daemons/constants/max_price_age.go | 8 + daemons/constants/price_fetcher.go | 11 + daemons/constants/pricefeed.go | 12 + daemons/constants/static_exchange_details.go | 43 + .../constants/static_exchange_query_config.go | 137 ++ .../constants/static_market_params_config.go | 27 + daemons/exchange_common/exchange_id.go | 40 + daemons/exchange_common/market_id.go | 6 + daemons/flags/flags.go | 141 ++ daemons/flags/flags_test.go | 77 + daemons/pricefeed/client/client.go | 330 +++ .../price_encoder/adjust_by_market_details.go | 11 + .../client/price_encoder/mutable_state.go | 76 + .../price_encoder/price_conversion_details.go | 12 + .../client/price_encoder/price_encoder.go | 405 ++++ .../client/price_fetcher/mutable_state.go | 125 ++ .../client/price_fetcher/price_fetcher.go | 344 ++++ .../price_fetcher/task_loop_definition.go | 22 + .../queryhandler/exchange_query_handler.go | 149 ++ .../client/sources/binance/binance.go | 59 + .../client/sources/binance/binance_test.go | 231 +++ .../sources/binance/exchange_query_details.go | 22 + .../binance/exchange_query_details_test.go | 24 + .../binance/testdata/btc_ticker_binance.json | 23 + .../binance/testdata/eth_ticker_binance.json | 23 + .../client/sources/bitfinex/bitfinex.go | 122 ++ .../bitfinex/exchange_query_details.go | 15 + .../bitfinex/exchange_query_details_test.go | 16 + .../client/sources/bitstamp/bitstamp.go | 58 + .../client/sources/bitstamp/bitstamp_test.go | 209 ++ .../bitstamp/exchange_query_details.go | 15 + .../bitstamp/exchange_query_details_test.go | 16 + .../pricefeed/client/sources/bybit/bybit.go | 74 + .../client/sources/bybit/bybit_test.go | 217 ++ .../sources/bybit/exchange_query_details.go | 15 + .../bybit/exchange_query_details_test.go | 16 + .../sources/coinbase_pro/coinbase_pro.go | 72 + .../sources/coinbase_pro/coinbase_pro_test.go | 187 ++ .../coinbase_pro/exchange_query_details.go | 14 + .../exchange_query_details_test.go | 16 + .../coinbase_pro/testdata/btc_ticker.json | 9 + .../client/sources/crypto_com/crypto_com.go | 76 + .../sources/crypto_com/crypto_com_test.go | 238 +++ .../crypto_com/exchange_query_details.go | 15 + .../crypto_com/exchange_query_details_test.go | 16 + .../client/sources/exchange_error.go | 41 + .../client/sources/exchange_error_test.go | 18 + .../sources/gate/exchange_query_details.go | 15 + .../gate/exchange_query_details_test.go | 16 + daemons/pricefeed/client/sources/gate/gate.go | 58 + .../client/sources/gate/gate_test.go | 214 ++ .../sources/huobi/exchange_query_details.go | 15 + .../huobi/exchange_query_details_test.go | 16 + .../pricefeed/client/sources/huobi/huobi.go | 69 + .../client/sources/huobi/huobi_test.go | 237 +++ .../sources/huobi/testdata/btc_ticker.json | 14 + .../sources/huobi/testdata/eth_ticker.json | 14 + .../sources/kraken/exchange_query_details.go | 15 + .../kraken/exchange_query_details_test.go | 16 + .../pricefeed/client/sources/kraken/kraken.go | 92 + .../client/sources/kraken/kraken_test.go | 124 ++ .../testdata/kraken_2_ticker_response.json | 79 + .../sources/kucoin/exchange_query_details.go | 15 + .../kucoin/exchange_query_details_test.go | 16 + .../pricefeed/client/sources/kucoin/kucoin.go | 74 + .../client/sources/kucoin/kucoin_test.go | 238 +++ .../sources/kucoin/testdata/btc_ticker.json | 18 + .../sources/kucoin/testdata/eth_ticker.json | 18 + .../sources/mexc/exchange_query_details.go | 15 + .../mexc/exchange_query_details_test.go | 16 + daemons/pricefeed/client/sources/mexc/mexc.go | 69 + .../client/sources/mexc/mexc_test.go | 237 +++ .../sources/mexc/testdata/btc_ticker.json | 13 + .../sources/mexc/testdata/eth_ticker.json | 13 + .../sources/okx/exchange_query_details.go | 15 + .../okx/exchange_query_details_test.go | 16 + daemons/pricefeed/client/sources/okx/okx.go | 69 + .../pricefeed/client/sources/okx/okx_test.go | 237 +++ .../sources/okx/testdata/btc_ticker.json | 18 + .../sources/okx/testdata/eth_ticker.json | 18 + .../exchange_query_details.go | 33 + .../fixed_price_function.go | 56 + .../exchange_query_details.go | 29 + .../volatile_price_function.go | 87 + .../testexchange/exchange_query_details.go | 20 + .../client/sources/testutil/test_util.go | 24 + daemons/pricefeed/client/sources/util.go | 249 +++ daemons/pricefeed/client/sources/util_test.go | 433 ++++ daemons/pricefeed/client/subtask_runner.go | 320 +++ .../client/types/exchange_config_json.go | 36 + daemons/pricefeed/client/types/exchange_id.go | 4 + .../types/exchange_market_config_json.go | 45 + .../client/types/exchange_query_config.go | 20 + .../client/types/exchange_query_details.go | 26 + .../client/types/exchange_to_market_prices.go | 137 ++ daemons/pricefeed/client/types/exponent.go | 4 + .../pricefeed/client/types/market_config.go | 65 + daemons/pricefeed/client/types/market_id.go | 4 + .../pricefeed/client/types/market_param.go | 70 + .../client/types/market_price_timestamp.go | 10 + .../pricefeed/client/types/market_to_price.go | 81 + .../types/mutable_exchange_config_updater.go | 17 + .../types/mutable_exchange_market_config.go | 84 + .../client/types/mutable_market_config.go | 32 + .../price_feed_mutable_market_configs.go | 583 ++++++ daemons/pricefeed/metrics/market_pairs.go | 47 + .../pricefeed/metrics/market_pairs_test.go | 31 + daemons/pricefeed/metrics/metrics.go | 24 + daemons/pricefeed/metrics/metrics_test.go | 42 + daemons/pricefeed/types/price_timestamp.go | 38 + daemons/pricefeed/types/resolver.go | 5 + daemons/server/pricefeed.go | 124 ++ daemons/server/pricefeed_test.go | 193 ++ daemons/server/server.go | 104 + daemons/server/server_test.go | 210 ++ daemons/server/types/constants.go | 17 + daemons/server/types/pricefeed.pb.go | 1100 ++++++++++ .../types/pricefeed/exchange_to_price.go | 95 + .../types/pricefeed/exchange_to_price_test.go | 157 ++ .../pricefeed/market_to_exchange_prices.go | 127 ++ .../market_to_exchange_prices_test.go | 196 ++ daemons/server/types/stoppable.go | 7 + daemons/types/file_handler.go | 21 + daemons/types/grpc_client.go | 62 + daemons/types/grpc_server.go | 17 + daemons/types/health_checkable.go | 156 ++ daemons/types/request_handler.go | 33 + docs/static/openapi.yml | 1831 ++++++++++++++--- go.mod | 19 +- go.sum | 25 +- lib/big_math.go | 55 + lib/collections.go | 18 + lib/constants.go | 16 + lib/convert.go | 55 + lib/math.go | 62 + lib/metrics/constants.go | 153 ++ lib/metrics/labels.go | 12 + lib/metrics/lib.go | 92 + lib/metrics/metric_keys.go | 22 + lib/metrics/util.go | 82 + lib/metrics/util_test.go | 244 +++ lib/prices/utils.go | 101 + lib/sortable.go | 26 + lib/time/time_provider.go | 21 + mocks/AppOptions.go | 41 + proto/layer/daemons/pricefeed.proto | 38 + proto/layer/oracle/median_values.proto | 10 + proto/layer/oracle/query.proto | 19 +- testutil/big/big.go | 30 + testutil/constants/prices.go | 6 + .../pricefeed/exchange_config/market_id.go | 83 + .../testnet_exchange_market_config.go | 1103 ++++++++++ testutil/daemons/pricefeed/pricefeed_utils.go | 38 + x/oracle/client/cli/query.go | 2 + .../client/cli/query_get_median_values.go | 42 + x/oracle/keeper/keeper.go | 46 +- x/oracle/keeper/keeper_test.go | 2 + x/oracle/keeper/query_get_median_values.go | 32 + x/oracle/module.go | 2 + x/oracle/types/median_values.pb.go | 335 +++ x/oracle/types/query.pb.go | 475 ++++- x/oracle/types/query.pb.gw.go | 65 + 178 files changed, 16895 insertions(+), 501 deletions(-) create mode 100644 app/flags/flags.go create mode 100644 cmd/layerd/cmd/init.go create mode 100644 cmd/layerd/cmd/price.go create mode 100644 daemons/configs/default_config.go create mode 100644 daemons/configs/default_config_test.go create mode 100644 daemons/configs/default_market_param_config.go create mode 100644 daemons/configs/test_data/broken_test.toml create mode 100644 daemons/configs/test_data/missingvals_test.toml create mode 100644 daemons/configs/test_data/valid_test.toml create mode 100644 daemons/configs/test_data/wrongvaltype_test.toml create mode 100644 daemons/constants/ipc.go create mode 100644 daemons/constants/logger.go create mode 100644 daemons/constants/max_price_age.go create mode 100644 daemons/constants/price_fetcher.go create mode 100644 daemons/constants/pricefeed.go create mode 100644 daemons/constants/static_exchange_details.go create mode 100644 daemons/constants/static_exchange_query_config.go create mode 100644 daemons/constants/static_market_params_config.go create mode 100644 daemons/exchange_common/exchange_id.go create mode 100644 daemons/exchange_common/market_id.go create mode 100644 daemons/flags/flags.go create mode 100644 daemons/flags/flags_test.go create mode 100644 daemons/pricefeed/client/client.go create mode 100644 daemons/pricefeed/client/price_encoder/adjust_by_market_details.go create mode 100644 daemons/pricefeed/client/price_encoder/mutable_state.go create mode 100644 daemons/pricefeed/client/price_encoder/price_conversion_details.go create mode 100644 daemons/pricefeed/client/price_encoder/price_encoder.go create mode 100644 daemons/pricefeed/client/price_fetcher/mutable_state.go create mode 100644 daemons/pricefeed/client/price_fetcher/price_fetcher.go create mode 100644 daemons/pricefeed/client/price_fetcher/task_loop_definition.go create mode 100644 daemons/pricefeed/client/queryhandler/exchange_query_handler.go create mode 100644 daemons/pricefeed/client/sources/binance/binance.go create mode 100644 daemons/pricefeed/client/sources/binance/binance_test.go create mode 100644 daemons/pricefeed/client/sources/binance/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/binance/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/binance/testdata/btc_ticker_binance.json create mode 100644 daemons/pricefeed/client/sources/binance/testdata/eth_ticker_binance.json create mode 100644 daemons/pricefeed/client/sources/bitfinex/bitfinex.go create mode 100644 daemons/pricefeed/client/sources/bitfinex/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/bitfinex/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/bitstamp/bitstamp.go create mode 100644 daemons/pricefeed/client/sources/bitstamp/bitstamp_test.go create mode 100644 daemons/pricefeed/client/sources/bitstamp/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/bitstamp/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/bybit/bybit.go create mode 100644 daemons/pricefeed/client/sources/bybit/bybit_test.go create mode 100644 daemons/pricefeed/client/sources/bybit/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/bybit/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro.go create mode 100644 daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro_test.go create mode 100644 daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/coinbase_pro/testdata/btc_ticker.json create mode 100644 daemons/pricefeed/client/sources/crypto_com/crypto_com.go create mode 100644 daemons/pricefeed/client/sources/crypto_com/crypto_com_test.go create mode 100644 daemons/pricefeed/client/sources/crypto_com/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/crypto_com/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/exchange_error.go create mode 100644 daemons/pricefeed/client/sources/exchange_error_test.go create mode 100644 daemons/pricefeed/client/sources/gate/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/gate/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/gate/gate.go create mode 100644 daemons/pricefeed/client/sources/gate/gate_test.go create mode 100644 daemons/pricefeed/client/sources/huobi/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/huobi/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/huobi/huobi.go create mode 100644 daemons/pricefeed/client/sources/huobi/huobi_test.go create mode 100644 daemons/pricefeed/client/sources/huobi/testdata/btc_ticker.json create mode 100644 daemons/pricefeed/client/sources/huobi/testdata/eth_ticker.json create mode 100644 daemons/pricefeed/client/sources/kraken/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/kraken/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/kraken/kraken.go create mode 100644 daemons/pricefeed/client/sources/kraken/kraken_test.go create mode 100644 daemons/pricefeed/client/sources/kraken/testdata/kraken_2_ticker_response.json create mode 100644 daemons/pricefeed/client/sources/kucoin/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/kucoin/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/kucoin/kucoin.go create mode 100644 daemons/pricefeed/client/sources/kucoin/kucoin_test.go create mode 100644 daemons/pricefeed/client/sources/kucoin/testdata/btc_ticker.json create mode 100644 daemons/pricefeed/client/sources/kucoin/testdata/eth_ticker.json create mode 100644 daemons/pricefeed/client/sources/mexc/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/mexc/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/mexc/mexc.go create mode 100644 daemons/pricefeed/client/sources/mexc/mexc_test.go create mode 100644 daemons/pricefeed/client/sources/mexc/testdata/btc_ticker.json create mode 100644 daemons/pricefeed/client/sources/mexc/testdata/eth_ticker.json create mode 100644 daemons/pricefeed/client/sources/okx/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/okx/exchange_query_details_test.go create mode 100644 daemons/pricefeed/client/sources/okx/okx.go create mode 100644 daemons/pricefeed/client/sources/okx/okx_test.go create mode 100644 daemons/pricefeed/client/sources/okx/testdata/btc_ticker.json create mode 100644 daemons/pricefeed/client/sources/okx/testdata/eth_ticker.json create mode 100644 daemons/pricefeed/client/sources/test_fixed_price_exchange/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/test_fixed_price_exchange/fixed_price_function.go create mode 100644 daemons/pricefeed/client/sources/test_volatile_exchange/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/test_volatile_exchange/volatile_price_function.go create mode 100644 daemons/pricefeed/client/sources/testexchange/exchange_query_details.go create mode 100644 daemons/pricefeed/client/sources/testutil/test_util.go create mode 100644 daemons/pricefeed/client/sources/util.go create mode 100644 daemons/pricefeed/client/sources/util_test.go create mode 100644 daemons/pricefeed/client/subtask_runner.go create mode 100644 daemons/pricefeed/client/types/exchange_config_json.go create mode 100644 daemons/pricefeed/client/types/exchange_id.go create mode 100644 daemons/pricefeed/client/types/exchange_market_config_json.go create mode 100644 daemons/pricefeed/client/types/exchange_query_config.go create mode 100644 daemons/pricefeed/client/types/exchange_query_details.go create mode 100644 daemons/pricefeed/client/types/exchange_to_market_prices.go create mode 100644 daemons/pricefeed/client/types/exponent.go create mode 100644 daemons/pricefeed/client/types/market_config.go create mode 100644 daemons/pricefeed/client/types/market_id.go create mode 100644 daemons/pricefeed/client/types/market_param.go create mode 100644 daemons/pricefeed/client/types/market_price_timestamp.go create mode 100644 daemons/pricefeed/client/types/market_to_price.go create mode 100644 daemons/pricefeed/client/types/mutable_exchange_config_updater.go create mode 100644 daemons/pricefeed/client/types/mutable_exchange_market_config.go create mode 100644 daemons/pricefeed/client/types/mutable_market_config.go create mode 100644 daemons/pricefeed/client/types/price_feed_mutable_market_configs.go create mode 100644 daemons/pricefeed/metrics/market_pairs.go create mode 100644 daemons/pricefeed/metrics/market_pairs_test.go create mode 100644 daemons/pricefeed/metrics/metrics.go create mode 100644 daemons/pricefeed/metrics/metrics_test.go create mode 100644 daemons/pricefeed/types/price_timestamp.go create mode 100644 daemons/pricefeed/types/resolver.go create mode 100644 daemons/server/pricefeed.go create mode 100644 daemons/server/pricefeed_test.go create mode 100644 daemons/server/server.go create mode 100644 daemons/server/server_test.go create mode 100644 daemons/server/types/constants.go create mode 100644 daemons/server/types/pricefeed.pb.go create mode 100644 daemons/server/types/pricefeed/exchange_to_price.go create mode 100644 daemons/server/types/pricefeed/exchange_to_price_test.go create mode 100644 daemons/server/types/pricefeed/market_to_exchange_prices.go create mode 100644 daemons/server/types/pricefeed/market_to_exchange_prices_test.go create mode 100644 daemons/server/types/stoppable.go create mode 100644 daemons/types/file_handler.go create mode 100644 daemons/types/grpc_client.go create mode 100644 daemons/types/grpc_server.go create mode 100644 daemons/types/health_checkable.go create mode 100644 daemons/types/request_handler.go create mode 100644 lib/big_math.go create mode 100644 lib/collections.go create mode 100644 lib/constants.go create mode 100644 lib/convert.go create mode 100644 lib/math.go create mode 100644 lib/metrics/constants.go create mode 100644 lib/metrics/labels.go create mode 100644 lib/metrics/lib.go create mode 100644 lib/metrics/metric_keys.go create mode 100644 lib/metrics/util.go create mode 100644 lib/metrics/util_test.go create mode 100644 lib/prices/utils.go create mode 100644 lib/sortable.go create mode 100644 lib/time/time_provider.go create mode 100644 mocks/AppOptions.go create mode 100644 proto/layer/daemons/pricefeed.proto create mode 100644 proto/layer/oracle/median_values.proto create mode 100644 testutil/big/big.go create mode 100644 testutil/constants/prices.go create mode 100644 testutil/daemons/pricefeed/exchange_config/market_id.go create mode 100644 testutil/daemons/pricefeed/exchange_config/testnet_exchange_market_config.go create mode 100644 testutil/daemons/pricefeed/pricefeed_utils.go create mode 100644 x/oracle/client/cli/query_get_median_values.go create mode 100644 x/oracle/keeper/query_get_median_values.go create mode 100644 x/oracle/types/median_values.pb.go diff --git a/app/app.go b/app/app.go index 520882fb0..e2013325f 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,7 @@ package app import ( + "context" "encoding/json" "fmt" "io" @@ -109,6 +110,7 @@ import ( "github.com/tellor-io/layer/x/mint" mintkeeper "github.com/tellor-io/layer/x/mint/keeper" minttypes "github.com/tellor-io/layer/x/mint/types" + "google.golang.org/grpc" oraclemodule "github.com/tellor-io/layer/x/oracle" oraclemodulekeeper "github.com/tellor-io/layer/x/oracle/keeper" @@ -125,6 +127,16 @@ import ( appparams "github.com/tellor-io/layer/app/params" "github.com/tellor-io/layer/docs" + + "github.com/tellor-io/layer/app/flags" + "github.com/tellor-io/layer/daemons/configs" + "github.com/tellor-io/layer/daemons/constants" + daemonflags "github.com/tellor-io/layer/daemons/flags" + daemonserver "github.com/tellor-io/layer/daemons/server" + daemontypes "github.com/tellor-io/layer/daemons/types" + + pricefeedclient "github.com/tellor-io/layer/daemons/pricefeed/client" + pricefeedtypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" ) const ( @@ -166,9 +178,9 @@ var ( auth.AppModuleBasic{}, authzmodule.AppModuleBasic{}, genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), - bankModule{}, + bank.AppModuleBasic{}, capability.AppModuleBasic{}, - stakingModule{}, + staking.AppModuleBasic{}, mint.AppModuleBasic{}, distr.AppModuleBasic{}, gov.NewAppModuleBasic(getGovProposalHandlers()), @@ -281,6 +293,10 @@ type App struct { // sm is the simulation manager sm *module.SimulationManager configurator module.Configurator + + Server *daemonserver.Server + startDaemons func() + PriceFeedClient *pricefeedclient.Client } // New returns a reference to an initialized blockchain app @@ -548,12 +564,51 @@ func New( app.GetSubspace(registrymoduletypes.ModuleName), ) registryModule := registrymodule.NewAppModule(appCodec, app.RegistryKeeper, app.AccountKeeper, app.BankKeeper) + indexPriceCache := pricefeedtypes.NewMarketToExchangePrices(constants.MaxPriceAge) + + // this line is used by starport scaffolding # stargate/app/keeperDefinition + appFlags := flags.GetFlagValuesFromOptions(appOpts) + logger.Info("Parsed App flags", "Flags", appFlags) + // Panic if this is not a full node and gRPC is disabled. + if err := appFlags.Validate(); err != nil { + panic(err) + } + + // Get Daemon Flags. + daemonFlags := daemonflags.GetDaemonFlagValuesFromOptions(appOpts) + logger.Info("Parsed Daemon flags", "Flags", daemonFlags) + // Create server that will ingest gRPC messages from daemon clients. + // Note that gRPC clients will block on new gRPC connection until the gRPC server is ready to + // accept new connections. + app.Server = daemonserver.NewServer( + logger, + grpc.NewServer(), + &daemontypes.FileHandlerImpl{}, + daemonFlags.Shared.SocketAddress, + ) + app.Server.WithPriceFeedMarketToExchangePrices(indexPriceCache) + + // Create a closure for starting daemons and daemon server. Daemon services are delayed until after the gRPC + // service is started because daemons depend on the gRPC service being available. If a node is initialized + // with a genesis time in the future, then the gRPC service will not be available until the genesis time, the + // daemons will not be able to connect to the cosmos gRPC query service and finish initialization, and the daemon + // monitoring service will panic. + app.startDaemons = func() { + // Start server for handling gRPC messages from daemons. + go app.Server.Start() + + } + // Non-validating full-nodes have no need to run the price daemon. + // if !appFlags.NonValidatingFullNode && daemonFlags.Price.Enabled { + exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) + marketParamsConfig := configs.ReadMarketParamsConfigFile(homePath) app.OracleKeeper = *oraclemodulekeeper.NewKeeper( appCodec, keys[oraclemoduletypes.StoreKey], keys[oraclemoduletypes.MemStoreKey], - + marketParamsConfig, + indexPriceCache, app.AccountKeeper, app.BankKeeper, app.DistrKeeper, @@ -576,9 +631,23 @@ func New( app.StakingKeeper, ) disputeModule := disputemodule.NewAppModule(appCodec, app.DisputeKeeper, app.AccountKeeper, app.BankKeeper) - - // this line is used by starport scaffolding # stargate/app/keeperDefinition - + // 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{}, + marketParamsConfig, + exchangeQueryConfig, + constants.StaticExchangeDetails, + &pricefeedclient.SubTaskRunnerImpl{}, + ) + // } /**** IBC Routing ****/ // Sealing prevents other modules from creating scoped sub-keepers @@ -926,6 +995,7 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig // register app's OpenAPI routes. docs.RegisterOpenAPIService(Name, apiSvr.Router) + app.startDaemons() } // RegisterTxService implements the Application.RegisterTxService method. @@ -946,6 +1016,7 @@ func (app *App) RegisterTendermintService(clientCtx client.Context) { // RegisterNodeService implements the Application.RegisterNodeService method. func (app *App) RegisterNodeService(clientCtx client.Context) { nodeservice.RegisterNodeService(clientCtx, app.GRPCQueryRouter()) + } // initParamsKeeper init params keeper and its subspaces diff --git a/app/default_overrides.go b/app/default_overrides.go index cdde0a76c..1a8a792d5 100644 --- a/app/default_overrides.go +++ b/app/default_overrides.go @@ -1,77 +1,77 @@ package app -import ( - "encoding/json" +// import ( +// "encoding/json" - "cosmossdk.io/math" +// "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/x/bank" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" +// "github.com/cosmos/cosmos-sdk/codec" +// "github.com/cosmos/cosmos-sdk/x/bank" +// banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +// distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" - "github.com/cosmos/cosmos-sdk/x/distribution" +// "github.com/cosmos/cosmos-sdk/x/distribution" - "github.com/cosmos/cosmos-sdk/x/staking" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" -) +// "github.com/cosmos/cosmos-sdk/x/staking" +// stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +// ) -// bankModule defines a custom wrapper around the x/bank module's AppModuleBasic -// implementation to provide custom default genesis state. -type bankModule struct { - bank.AppModuleBasic -} +// // bankModule defines a custom wrapper around the x/bank module's AppModuleBasic +// // implementation to provide custom default genesis state. +// type bankModule struct { +// bank.AppModuleBasic +// } -// DefaultGenesis returns custom x/bank module genesis state. -func (bankModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { - metadata := banktypes.Metadata{ - Description: "The native token of the Tellor Layer.", - Base: BondDenom, - Name: DisplayDenom, - Display: DisplayDenom, - Symbol: DisplayDenom, - DenomUnits: []*banktypes.DenomUnit{ - { - Denom: BondDenom, // ie Wei - Exponent: 0, - }, - { - Denom: DisplayDenom, //ie Ether - Exponent: 6, - }, - }, - } +// // DefaultGenesis returns custom x/bank module genesis state. +// func (bankModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { +// metadata := banktypes.Metadata{ +// Description: "The native token of the Tellor Layer.", +// Base: BondDenom, +// Name: DisplayDenom, +// Display: DisplayDenom, +// Symbol: DisplayDenom, +// DenomUnits: []*banktypes.DenomUnit{ +// { +// Denom: BondDenom, // ie Wei +// Exponent: 0, +// }, +// { +// Denom: DisplayDenom, //ie Ether +// Exponent: 6, +// }, +// }, +// } - genState := banktypes.DefaultGenesisState() - genState.DenomMetadata = append(genState.DenomMetadata, metadata) +// genState := banktypes.DefaultGenesisState() +// genState.DenomMetadata = append(genState.DenomMetadata, metadata) - return cdc.MustMarshalJSON(genState) -} +// return cdc.MustMarshalJSON(genState) +// } -// stakingModule wraps the x/staking module in order to overwrite specific -// ModuleManager APIs. -type stakingModule struct { - staking.AppModuleBasic -} +// // stakingModule wraps the x/staking module in order to overwrite specific +// // ModuleManager APIs. +// type stakingModule struct { +// staking.AppModuleBasic +// } -// DefaultGenesis returns custom x/staking module genesis state. -func (stakingModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { - params := stakingtypes.DefaultParams() - params.BondDenom = BondDenom +// // DefaultGenesis returns custom x/staking module genesis state. +// func (stakingModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { +// params := stakingtypes.DefaultParams() +// params.BondDenom = BondDenom - return cdc.MustMarshalJSON(&stakingtypes.GenesisState{ - Params: params, - }) -} +// return cdc.MustMarshalJSON(&stakingtypes.GenesisState{ +// Params: params, +// }) +// } -type distrModule struct { - distribution.AppModuleBasic -} +// type distrModule struct { +// distribution.AppModuleBasic +// } -// DefaultGenesis returns custom x/distribution module genesis state. -func (distrModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { - genState := distrtypes.DefaultGenesisState() - genState.Params.CommunityTax = math.LegacyZeroDec() // 0% community tax on gas fees, inflation is minted to timeBasedRewards for reporters +// // DefaultGenesis returns custom x/distribution module genesis state. +// func (distrModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { +// genState := distrtypes.DefaultGenesisState() +// genState.Params.CommunityTax = math.LegacyZeroDec() // 0% community tax on gas fees, inflation is minted to timeBasedRewards for reporters - return cdc.MustMarshalJSON(genState) -} +// return cdc.MustMarshalJSON(genState) +// } diff --git a/app/flags/flags.go b/app/flags/flags.go new file mode 100644 index 000000000..34c886cf4 --- /dev/null +++ b/app/flags/flags.go @@ -0,0 +1,136 @@ +package flags + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/spf13/cast" + "github.com/spf13/cobra" +) + +// A struct containing the values of all flags. +type Flags struct { + DdAgentHost string + DdTraceAgentPort uint16 + NonValidatingFullNode bool + DdErrorTrackingFormat bool + + // Existing flags + GrpcAddress string + GrpcEnable bool +} + +// List of CLI flags. +const ( + DdAgentHost = "dd-agent-host" + DdTraceAgentPort = "dd-trace-agent-port" + NonValidatingFullNodeFlag = "non-validating-full-node" + DdErrorTrackingFormat = "dd-error-tracking-format" + + // Cosmos flags below. These config values can be set as flags or in config.toml. + GrpcAddress = "grpc.address" + GrpcEnable = "grpc.enable" +) + +// Default values. +const ( + DefaultDdAgentHost = "" + DefaultDdTraceAgentPort = 8126 + DefaultNonValidatingFullNode = false + DefaultDdErrorTrackingFormat = false +) + +// AddFlagsToCmd adds flags to app initialization. +// These flags should be applied to the `start` command of the LAYER Cosmos application. +// E.g. `layerd start --non-validating-full-node true`. +func AddFlagsToCmd(cmd *cobra.Command) { + cmd.Flags().Bool( + NonValidatingFullNodeFlag, + DefaultNonValidatingFullNode, + "Whether to run in non-validating full-node mode. "+ + "This disables the pricing daemon and enables the full-node ProcessProposal logic. "+ + "Validators should _never_ use this mode.", + ) + cmd.Flags().String( + DdAgentHost, + DefaultDdAgentHost, + "Sets the address to connect to for the Datadog Agent.", + ) + cmd.Flags().Uint16( + DdTraceAgentPort, + DefaultDdTraceAgentPort, + "Sets the Datadog Agent port.", + ) + cmd.Flags().Bool( + DdErrorTrackingFormat, + DefaultDdErrorTrackingFormat, + "Enable formatting of log error tags to datadog error tracking format", + ) +} + +// Validate checks that the flags are valid. +func (f *Flags) Validate() error { + // Validtors must have cosmos grpc services enabled. + if !f.NonValidatingFullNode && !f.GrpcEnable { + return fmt.Errorf("grpc.enable must be set to true - validating requires gRPC server") + } + return nil +} + +// GetFlagValuesFromOptions gets values from the `AppOptions` struct which contains values +// from the command-line flags. +func GetFlagValuesFromOptions( + appOpts servertypes.AppOptions, +) Flags { + // Create default result. + result := Flags{ + NonValidatingFullNode: DefaultNonValidatingFullNode, + DdAgentHost: DefaultDdAgentHost, + DdTraceAgentPort: DefaultDdTraceAgentPort, + DdErrorTrackingFormat: DefaultDdErrorTrackingFormat, + + // These are the default values from the Cosmos flags. + GrpcAddress: config.DefaultGRPCAddress, + GrpcEnable: true, + } + + // Populate the flags if they exist. + if option := appOpts.Get(NonValidatingFullNodeFlag); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.NonValidatingFullNode = v + } + } + + if option := appOpts.Get(DdAgentHost); option != nil { + if v, err := cast.ToStringE(option); err == nil { + result.DdAgentHost = v + } + } + + if option := appOpts.Get(DdTraceAgentPort); option != nil { + if v, err := cast.ToUint16E(option); err == nil { + result.DdTraceAgentPort = v + } + } + + if option := appOpts.Get(DdErrorTrackingFormat); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.DdErrorTrackingFormat = v + } + } + + if option := appOpts.Get(GrpcAddress); option != nil { + if v, err := cast.ToStringE(option); err == nil { + result.GrpcAddress = v + } + } + + if option := appOpts.Get(GrpcEnable); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.GrpcEnable = v + } + } + + return result +} diff --git a/cmd/layerd/cmd/init.go b/cmd/layerd/cmd/init.go new file mode 100644 index 000000000..554ccec63 --- /dev/null +++ b/cmd/layerd/cmd/init.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "os" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" + "github.com/tellor-io/layer/daemons/configs" +) + +// AddInitCmdPostRunE adds a PostRunE to the `init` subcommand. +func AddInitCmdPostRunE(rootCmd *cobra.Command) { + // Fetch init subcommand. + initCmd, _, err := rootCmd.Find([]string{"init"}) + if err != nil { + os.Exit(1) + } + + // Add PostRun to configure required setups after `init`. + initCmd.PostRunE = func(cmd *cobra.Command, args []string) error { + // Get home directory. + clientCtx := client.GetClientContextFromCmd(cmd) + + // Add default pricefeed exchange config toml file if it does not exist. + configs.WriteDefaultPricefeedExchangeToml(clientCtx.HomeDir) + configs.WriteDefaultMarketParamsToml(clientCtx.HomeDir) + return nil + } +} diff --git a/cmd/layerd/cmd/price.go b/cmd/layerd/cmd/price.go new file mode 100644 index 000000000..e2c084cc5 --- /dev/null +++ b/cmd/layerd/cmd/price.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "github.com/spf13/cobra" + appflags "github.com/tellor-io/layer/app/flags" + daemonflags "github.com/tellor-io/layer/daemons/flags" +) + +// RootCmdOption configures root command option. +type RootCmdOption struct { + startCmdCustomizer func(*cobra.Command) +} + +// newRootCmdOption returns an empty RootCmdOption. +func newRootCmdOption() *RootCmdOption { + return &RootCmdOption{} +} + +// setCustomizeStartCmd accepts a handler to customize the start command and set it in the option. +func (o *RootCmdOption) setCustomizeStartCmd(f func(startCmd *cobra.Command)) { + o.startCmdCustomizer = f +} + +// GetOptionWithCustomStartCmd returns a root command option with custom start commands. +func GetOptionWithCustomStartCmd() *RootCmdOption { + option := newRootCmdOption() + f := func(cmd *cobra.Command) { + // Add app flags. + appflags.AddFlagsToCmd(cmd) + + // Add daemon flags. + daemonflags.AddDaemonFlagsToCmd(cmd) + + } + option.setCustomizeStartCmd(f) + return option +} diff --git a/cmd/layerd/cmd/root.go b/cmd/layerd/cmd/root.go index b5cb9872f..a86d432c4 100644 --- a/cmd/layerd/cmd/root.go +++ b/cmd/layerd/cmd/root.go @@ -44,7 +44,9 @@ import ( ) // NewRootCmd creates a new root command for a Cosmos SDK application -func NewRootCmd() (*cobra.Command, appparams.EncodingConfig) { +func NewRootCmd( + option *RootCmdOption, +) (*cobra.Command, appparams.EncodingConfig) { encodingConfig := app.MakeEncodingConfig() initClientCtx := client.Context{}. WithCodec(encodingConfig.Marshaler). @@ -84,7 +86,7 @@ func NewRootCmd() (*cobra.Command, appparams.EncodingConfig) { }, } - initRootCmd(rootCmd, encodingConfig) + initRootCmd(rootCmd, option, encodingConfig) overwriteFlagDefaults(rootCmd, map[string]string{ flags.FlagChainID: strings.ReplaceAll(app.Name, "-", ""), flags.FlagKeyringBackend: "test", @@ -102,6 +104,7 @@ func initTendermintConfig() *tmcfg.Config { func initRootCmd( rootCmd *cobra.Command, + option *RootCmdOption, encodingConfig appparams.EncodingConfig, ) { // Set config @@ -136,7 +139,13 @@ func initRootCmd( app.DefaultNodeHome, a.newApp, a.appExport, - addModuleInitFlags, + func(cmd *cobra.Command) { + addModuleInitFlags(cmd) + + if option.startCmdCustomizer != nil { + option.startCmdCustomizer(cmd) + } + }, ) // add keybase, auxiliary RPC, query, and tx child commands diff --git a/cmd/layerd/main.go b/cmd/layerd/main.go index 12f1b5074..64881f80a 100644 --- a/cmd/layerd/main.go +++ b/cmd/layerd/main.go @@ -11,7 +11,9 @@ import ( ) func main() { - rootCmd, _ := cmd.NewRootCmd() + option := cmd.GetOptionWithCustomStartCmd() + rootCmd, _ := cmd.NewRootCmd(option) + cmd.AddInitCmdPostRunE(rootCmd) if err := svrcmd.Execute(rootCmd, "", app.DefaultNodeHome); err != nil { switch e := err.(type) { case server.ErrorCode: diff --git a/daemons/configs/default_config.go b/daemons/configs/default_config.go new file mode 100644 index 000000000..7bb5e17f2 --- /dev/null +++ b/daemons/configs/default_config.go @@ -0,0 +1,123 @@ +package configs + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "text/template" + + tmos "github.com/cometbft/cometbft/libs/os" + "github.com/pelletier/go-toml" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// Note: any changes to the comments/variables/mapstructure must be reflected in the appropriate +// struct in daemons/pricefeed/client/static_exchange_startup_config.go. +const ( + defaultTomlTemplate = `# This is a TOML config file. + # StaticExchangeStartupConfig represents the mapping of exchanges to the parameters for + # querying from them. + # + # ExchangeId - Unique string identifying an exchange. + # + # IntervalMs - Delays between sending API requests to get exchange market prices - cannot be 0. + # + # TimeoutMs - Max time to wait on an API call to an exchange - cannot be 0. + # + # MaxQueries - Max api calls to get market prices for an exchange to make in a task-loop - + # cannot be 0. For multi-market API exchanges, the behavior will default to 1.{{ range $exchangeId, $element := .}} + [[exchanges]] + ExchangeId = "{{$element.ExchangeId}}" + IntervalMs = {{$element.IntervalMs}} + TimeoutMs = {{$element.TimeoutMs}} + MaxQueries = {{$element.MaxQueries}}{{end}} +` +) + +// GenerateDefaultExchangeTomlString creates the toml file string containing the default configs +// for querying each exchange. +func GenerateDefaultExchangeTomlString() bytes.Buffer { + // Create the template for turning each `parsableExchangeStartupConfig` into a toml map config in + // a stringified toml file. + template, err := template.New("").Parse(defaultTomlTemplate) + // Panic if failure occurs when parsing the template. + if err != nil { + panic(err) + } + + // Encode toml string into `defaultExchangeToml` and return if successful. Otherwise, panic. + var defaultExchangeToml bytes.Buffer + err = template.Execute(&defaultExchangeToml, constants.StaticExchangeQueryConfig) + if err != nil { + panic(err) + } + return defaultExchangeToml +} + +// WriteDefaultPricefeedExchangeToml reads in the toml string for the pricefeed client and +// writes said string to the config folder as a toml file if the config file does not exist. +func WriteDefaultPricefeedExchangeToml(homeDir string) { + // Write file into config folder if file does not exist. + configFilePath := getConfigFilePath(homeDir) + if !tmos.FileExists(configFilePath) { + buffer := GenerateDefaultExchangeTomlString() + tmos.MustWriteFile(configFilePath, buffer.Bytes(), 0644) + } +} + +// ReadExchangeQueryConfigFile gets a mapping of `exchangeIds` to `ExchangeQueryConfigs` +// where `ExchangeQueryConfig` for querying exchanges for market prices comes from parsing a TOML +// file in the config directory. +// NOTE: if the config file is not found for the price-daemon, return the static exchange query +// config. +func ReadExchangeQueryConfigFile(homeDir string) map[types.ExchangeId]*types.ExchangeQueryConfig { + // Read file for exchange query configurations. + tomlFile, err := os.ReadFile(getConfigFilePath(homeDir)) + if err != nil { + panic(fmt.Errorf("failed to read exchange query config file: %v, %v", tomlFile, err)) + } + + // Unmarshal `tomlFile` into `exchanges` for `exchangeStartupConfigMap`. + exchanges := map[string][]types.ExchangeQueryConfig{} + if err = toml.Unmarshal(tomlFile, &exchanges); err != nil { + panic(err) + } + + // Populate configs for exchanges. + exchangeStartupConfigMap := make(map[types.ExchangeId]*types.ExchangeQueryConfig, len(exchanges)) + for _, exchange := range exchanges["exchanges"] { + // Zero is an invalid configuration value for all parameters. This could also point to the + // configuration file being setup wrong with one or more exchange parameters unset. + if exchange.IntervalMs == 0 || + exchange.TimeoutMs == 0 || + exchange.MaxQueries == 0 { + panic( + fmt.Errorf( + "One or more query config values are unset or are set to zero for exchange with id: '%v'", + exchange.ExchangeId, + ), + ) + } + + // Insert Key-Value pair into `exchangeStartupConfigMap`. + exchangeStartupConfigMap[exchange.ExchangeId] = &types.ExchangeQueryConfig{ + ExchangeId: exchange.ExchangeId, + IntervalMs: exchange.IntervalMs, + TimeoutMs: exchange.TimeoutMs, + MaxQueries: exchange.MaxQueries, + } + } + + return exchangeStartupConfigMap +} + +// getConfigFilePath returns the path to the pricefeed exchange config file. +func getConfigFilePath(homeDir string) string { + return filepath.Join( + homeDir, + "config", + constants.PricefeedExchangeConfigFileName, + ) +} diff --git a/daemons/configs/default_config_test.go b/daemons/configs/default_config_test.go new file mode 100644 index 000000000..93c48a803 --- /dev/null +++ b/daemons/configs/default_config_test.go @@ -0,0 +1,247 @@ +package configs_test + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/tellor-io/layer/daemons/configs" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + + tmos "github.com/cometbft/cometbft/libs/os" + "github.com/stretchr/testify/require" +) + +var ( + binanceId = exchange_common.EXCHANGE_ID_BINANCE + filePath = fmt.Sprintf("config/%v", constants.PricefeedExchangeConfigFileName) +) + +const ( + tomlString = `# This is a TOML config file. + # StaticExchangeStartupConfig represents the mapping of exchanges to the parameters for + # querying from them. + # + # ExchangeId - Unique string identifying an exchange. + # + # IntervalMs - Delays between sending API requests to get exchange market prices - cannot be 0. + # + # TimeoutMs - Max time to wait on an API call to an exchange - cannot be 0. + # + # MaxQueries - Max api calls to get market prices for an exchange to make in a task-loop - + # cannot be 0. For multi-market API exchanges, the behavior will default to 1. + [[exchanges]] + ExchangeId = "Binance" + IntervalMs = 2500 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "BinanceUS" + IntervalMs = 2500 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Bitfinex" + IntervalMs = 2500 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Bitstamp" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Bybit" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "CoinbasePro" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 3 + [[exchanges]] + ExchangeId = "CryptoCom" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Gate" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Huobi" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Kraken" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Kucoin" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Mexc" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "Okx" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 1 + [[exchanges]] + ExchangeId = "TestFixedPriceExchange" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 3 + [[exchanges]] + ExchangeId = "TestVolatileExchange" + IntervalMs = 2000 + TimeoutMs = 3000 + MaxQueries = 3 +` +) + +func TestGenerateDefaultExchangeTomlString(t *testing.T) { + defaultConfigStringBuffer := configs.GenerateDefaultExchangeTomlString() + require.Equal( + t, + tomlString, + defaultConfigStringBuffer.String(), + ) +} + +func TestWriteDefaultPricefeedExchangeToml(t *testing.T) { + err := os.Mkdir("config", 0700) + require.NoError(t, err) + configs.WriteDefaultPricefeedExchangeToml("") + + buffer, err := os.ReadFile(filePath) + require.NoError(t, err) + + require.Equal(t, tomlString, string(buffer[:])) + os.RemoveAll("config") +} + +func TestWriteDefaultPricefeedExchangeToml_FileExists(t *testing.T) { + helloWorld := "Hello World" + + err := os.Mkdir("config", 0700) + require.NoError(t, err) + + tmos.MustWriteFile(filePath, bytes.NewBuffer([]byte(helloWorld)).Bytes(), 0644) + configs.WriteDefaultPricefeedExchangeToml("") + + buffer, err := os.ReadFile(filePath) + require.NoError(t, err) + + require.Equal(t, helloWorld, string(buffer[:])) + os.RemoveAll("config") +} + +func TestReadExchangeStartupConfigFile(t *testing.T) { + pwd, _ := os.Getwd() + + tests := map[string]struct { + // parameters + exchangeConfigSourcePath string + doNotWriteFile bool + + // expectations + expectedExchangeId types.ExchangeId + expectedIntervalMsExchange uint32 + expectedTimeoutMsExchange uint32 + expectedMaxQueries uint32 + expectedPanic error + }{ + "valid": { + exchangeConfigSourcePath: "test_data/valid_test.toml", + expectedExchangeId: binanceId, + expectedIntervalMsExchange: constants.StaticExchangeQueryConfig[binanceId].IntervalMs, + expectedTimeoutMsExchange: constants.StaticExchangeQueryConfig[binanceId].TimeoutMs, + expectedMaxQueries: constants.StaticExchangeQueryConfig[binanceId].MaxQueries, + }, + "config file cannot be found": { + exchangeConfigSourcePath: "test_data/notexisting_test.toml", + doNotWriteFile: true, + expectedPanic: fmt.Errorf( + "open %s%s: no such file or directory", + pwd+"/config/", + constants.PricefeedExchangeConfigFileName, + ), + }, + "config file cannot be unmarshalled": { + exchangeConfigSourcePath: "test_data/broken_test.toml", + expectedPanic: errors.New("(1, 12): was expecting token [[, but got unclosed table array key instead"), + }, + "config file has malformed values": { + exchangeConfigSourcePath: "test_data/missingvals_test.toml", + expectedPanic: errors.New( + "One or more query config values are unset or are set to zero for exchange with id: 'BinanceUS'", + ), + }, + "config file has incorrect values": { + exchangeConfigSourcePath: "test_data/wrongvaltype_test.toml", + expectedPanic: errors.New( + "(3, 1): Can't convert a(string) to uint32", + ), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if !tc.doNotWriteFile { + err := os.Mkdir("config", 0700) + require.NoError(t, err) + + file, err := os.Open(tc.exchangeConfigSourcePath) + require.NoError(t, err) + + config, err := os.Create(filepath.Join("config", constants.PricefeedExchangeConfigFileName)) + require.NoError(t, err) + _, err = config.ReadFrom(file) + require.NoError(t, err) + } + + if tc.expectedPanic != nil { + require.PanicsWithError( + t, + tc.expectedPanic.Error(), + func() { configs.ReadExchangeQueryConfigFile(pwd) }, + ) + + os.RemoveAll("config") + return + } + + exchangeStartupConfigMap := configs.ReadExchangeQueryConfigFile(pwd) + + require.Equal( + t, + &types.ExchangeQueryConfig{ + ExchangeId: tc.expectedExchangeId, + IntervalMs: tc.expectedIntervalMsExchange, + TimeoutMs: tc.expectedTimeoutMsExchange, + MaxQueries: tc.expectedMaxQueries, + }, + exchangeStartupConfigMap[tc.expectedExchangeId], + ) + + os.RemoveAll("config") + }) + } + + // In case tests fail and the path was never removed. + os.RemoveAll("config") +} diff --git a/daemons/configs/default_market_param_config.go b/daemons/configs/default_market_param_config.go new file mode 100644 index 000000000..5eb8020d7 --- /dev/null +++ b/daemons/configs/default_market_param_config.go @@ -0,0 +1,118 @@ +package configs + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "text/template" + + tmos "github.com/cometbft/cometbft/libs/os" + "github.com/pelletier/go-toml" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// Note: any changes to the comments/variables/mapstructure must be reflected in the appropriate +// struct in daemons/pricefeed/client/static_market_param_startup_config.go. +const ( + defaultMarketParamTomlTemplate = `# This is a TOML config file. + # StaticMarketParamStartupConfig represents the mapping of exchanges to the parameters for + # querying from them. + # + # market_params - Unique string identifying an exchange. + # + # Id - Delays between sending API requests to get exchange market prices - cannot be 0. + # + # MinExchanges - The minimum number of exchanges that should be reporting a live price for + # a price update to be considered valid. + # + # Exponent - The exponent of the price. + # + # Pair - The human-readable name of the market pair (e.g. "BTC-USD").{{ range $exchangeId, $element := .}} + [[market_params]] + ExchangeConfigJson = "{{$element.ExchangeConfigJson}}" + Exponent = {{$element.Exponent}} + Id = {{$element.Id}} + MinExchanges = {{$element.MinExchanges}} + MinPriceChangePpm = {{$element.MinPriceChangePpm}} + Pair = {{$element.Pair}}{{end}} +` +) + +// GenerateDefaultExchangeTomlString creates the toml file string containing the default marketParam configs. +func GenerateDefaultMarketParamsTomlString() bytes.Buffer { + template, err := template.New("").Parse(defaultMarketParamTomlTemplate) + // Panic if failure occurs when parsing the template. + if err != nil { + panic(err) + } + + // Encode toml string into `defaultMarketParamsToml` and return if successful. Otherwise, panic. + var defaultMarketParamsToml bytes.Buffer + err = template.Execute(&defaultMarketParamsToml, constants.StaticMarketParamsConfig) + if err != nil { + panic(err) + } + return defaultMarketParamsToml +} + +func WriteDefaultMarketParamsToml(homeDir string) { + // Write file into config folder if file does not exist. + configFilePath := getMarketParamsConfigFilePath(homeDir) + if !tmos.FileExists(configFilePath) { + buffer := GenerateDefaultMarketParamsTomlString() + tmos.MustWriteFile(configFilePath, buffer.Bytes(), 0644) + } +} + +func ReadMarketParamsConfigFile(homeDir string) []types.MarketParam { + // Read file for exchange query configurations. + tomlFile, err := os.ReadFile(getMarketParamsConfigFilePath(homeDir)) + if err != nil { + panic(fmt.Errorf("failed to read exchange query config file: %v, %v", tomlFile, err)) + } + + params := map[string][]types.MarketParam{} + if err = toml.Unmarshal(tomlFile, ¶ms); err != nil { + panic(err) + } + + paramStartupConfigMap := make(map[uint32]*types.MarketParam, len(params)) + for _, param := range params["market_params"] { + if param.Exponent == 0 || + param.MinExchanges == 0 || + param.MinPriceChangePpm == 0 { + panic( + fmt.Errorf( + "One or more config values are unset or are set to zero for pair with id: '%v'", + param.Id, + ), + ) + } + + // Insert Key-Value pair into `exchangeStartupConfigMap`. + paramStartupConfigMap[param.Id] = &types.MarketParam{ + ExchangeConfigJson: param.ExchangeConfigJson, + Exponent: param.Exponent, + Id: param.Id, + MinExchanges: param.MinExchanges, + MinPriceChangePpm: param.MinPriceChangePpm, + Pair: param.Pair, + } + } + marketParams := make([]types.MarketParam, 0, len(paramStartupConfigMap)) + for _, param := range paramStartupConfigMap { + marketParams = append(marketParams, *param) + } + return marketParams +} + +// getConfigFilePath returns the path to the pricefeed exchange config file. +func getMarketParamsConfigFilePath(homeDir string) string { + return filepath.Join( + homeDir, + "config", + constants.MarketParamsConfigFileName, + ) +} diff --git a/daemons/configs/test_data/broken_test.toml b/daemons/configs/test_data/broken_test.toml new file mode 100644 index 000000000..b7f29458e --- /dev/null +++ b/daemons/configs/test_data/broken_test.toml @@ -0,0 +1,6 @@ +[[exchanges] +ExchangeId = "BinanceUS" +IntervalMs = 4_250 +TimeoutMs = 3_000 +MaxQueries = 1 +MaxBufferSize = 10 diff --git a/daemons/configs/test_data/missingvals_test.toml b/daemons/configs/test_data/missingvals_test.toml new file mode 100644 index 000000000..eaf52e63b --- /dev/null +++ b/daemons/configs/test_data/missingvals_test.toml @@ -0,0 +1,4 @@ +[[exchanges]] +ExchangeId = "BinanceUS" +IntervalMs = 4_250 +TimeoutMs = 3_000 diff --git a/daemons/configs/test_data/valid_test.toml b/daemons/configs/test_data/valid_test.toml new file mode 100644 index 000000000..3d2cafc02 --- /dev/null +++ b/daemons/configs/test_data/valid_test.toml @@ -0,0 +1,5 @@ +[[exchanges]] +ExchangeId = "Binance" +IntervalMs = 2_500 +TimeoutMs = 3_000 +MaxQueries = 1 diff --git a/daemons/configs/test_data/wrongvaltype_test.toml b/daemons/configs/test_data/wrongvaltype_test.toml new file mode 100644 index 000000000..fd076b4b0 --- /dev/null +++ b/daemons/configs/test_data/wrongvaltype_test.toml @@ -0,0 +1,6 @@ +[[exchanges]] +ExchangeId = "BinanceUS" +IntervalMs = "a" +TimeoutMs = 3_000 +MaxQueries = 1 +MaxBufferSize = 10 diff --git a/daemons/constants/ipc.go b/daemons/constants/ipc.go new file mode 100644 index 000000000..628d63e34 --- /dev/null +++ b/daemons/constants/ipc.go @@ -0,0 +1,6 @@ +package constants + +const ( + // UnixProtocol is the network for gRPC protocol used by the price daemon and server. + UnixProtocol = "unix" +) diff --git a/daemons/constants/logger.go b/daemons/constants/logger.go new file mode 100644 index 000000000..c795e0f05 --- /dev/null +++ b/daemons/constants/logger.go @@ -0,0 +1,35 @@ +package constants + +import ( + "fmt" + "time" +) + +const ( + // Log keys are used to provide a consistent key-value interface for logging across the daemon. + SubmoduleLogKey = "submodule" + + ErrorLogKey = "error" + ExchangeIdLogKey = "exchangeId" + MarketIdLogKey = "marketId" + PriceLogKey = "Price" + ReasonLogKey = "reason" + + // Module and Submodule names are used to provide consistent key-value pairs for logging across the daemon. + PricefeedDaemonModuleName = "pricefeed-daemon" + PriceFetcherSubmoduleName = "price-fetcher" + PriceEncoderSubmoduleName = "price-encoder" + PriceUpdaterSubmoduleName = "price-updater" + MarketParamUpdaterSubmoduleName = "market-param-updater" + + // PriceDaemonStartupErrorGracePeriod defines the amount of time the daemon waits before logging issues that are + // intermittent on daemon startup as true errors. Examples of this includes price conversion failures due to + // an uninitialized prices cache, and failures to fetch market param updates due to a delay on the protocol side + // in starting the prices query service. + // If the protocol is not started within this grace period, the daemon will report these errors as true errors. + PriceDaemonStartupErrorGracePeriod = 120 * time.Second +) + +var ( + RateLimitingError = fmt.Errorf("status 429 - rate limit exceeded") +) diff --git a/daemons/constants/max_price_age.go b/daemons/constants/max_price_age.go new file mode 100644 index 000000000..fab16dfc0 --- /dev/null +++ b/daemons/constants/max_price_age.go @@ -0,0 +1,8 @@ +package constants + +import "time" + +const ( + // MaxPriceAge defines the duration in which a price update is valid for. + MaxPriceAge = time.Duration(30_000_000_000) // 30 sec, duration uses nanoseconds. +) diff --git a/daemons/constants/price_fetcher.go b/daemons/constants/price_fetcher.go new file mode 100644 index 000000000..b427287d2 --- /dev/null +++ b/daemons/constants/price_fetcher.go @@ -0,0 +1,11 @@ +package constants + +const ( + // 5K is chosen to be >> than the number of messages an exchange could send in any period before the + // price encoder is able to read the messages from the buffer, even if we add O(10-100) markets dynamically, + // but not large enough to allow more than at most a few minutes of price messages to accumulate. + FixedBufferSize = 1024 * 5 + // https://stackoverflow.com/questions/37774624/go-http-get-concurrency-and-connection-reset-by-peer. + // This is a good number to start with based on the above link. Adjustments can/will be made accordingly. + MaxConnectionsPerExchange = 50 +) diff --git a/daemons/constants/pricefeed.go b/daemons/constants/pricefeed.go new file mode 100644 index 000000000..e7b9a46ae --- /dev/null +++ b/daemons/constants/pricefeed.go @@ -0,0 +1,12 @@ +package constants + +const ( + // DefaultPrice is the default value for `Price` field in `UpdateMarketPricesRequest`. + DefaultPrice = 0 + + // PricefeedExchangeConfigFileName names the config file containing the exchange startup config. + PricefeedExchangeConfigFileName = "pricefeed_exchange_config.toml" + + // MarketParamsConfigFileName names the config file containing the market params. + MarketParamsConfigFileName = "market_params.toml" +) diff --git a/daemons/constants/static_exchange_details.go b/daemons/constants/static_exchange_details.go new file mode 100644 index 000000000..a0a2bc25e --- /dev/null +++ b/daemons/constants/static_exchange_details.go @@ -0,0 +1,43 @@ +package constants + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/binance" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitfinex" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitstamp" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bybit" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/coinbase_pro" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/crypto_com" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/gate" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/huobi" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kraken" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kucoin" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/mexc" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/okx" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/test_fixed_price_exchange" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/test_volatile_exchange" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testexchange" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + // StaticExchangeDetails is the static mapping of `ExchangeId` to its `ExchangeQueryDetails`. + StaticExchangeDetails = map[types.ExchangeId]types.ExchangeQueryDetails{ + exchange_common.EXCHANGE_ID_BINANCE: binance.BinanceDetails, + exchange_common.EXCHANGE_ID_BINANCE_US: binance.BinanceUSDetails, + exchange_common.EXCHANGE_ID_BITFINEX: bitfinex.BitfinexDetails, + exchange_common.EXCHANGE_ID_KRAKEN: kraken.KrakenDetails, + exchange_common.EXCHANGE_ID_GATE: gate.GateDetails, + exchange_common.EXCHANGE_ID_BITSTAMP: bitstamp.BitstampDetails, + exchange_common.EXCHANGE_ID_BYBIT: bybit.BybitDetails, + exchange_common.EXCHANGE_ID_CRYPTO_COM: crypto_com.CryptoComDetails, + exchange_common.EXCHANGE_ID_HUOBI: huobi.HuobiDetails, + exchange_common.EXCHANGE_ID_KUCOIN: kucoin.KucoinDetails, + exchange_common.EXCHANGE_ID_OKX: okx.OkxDetails, + exchange_common.EXCHANGE_ID_MEXC: mexc.MexcDetails, + exchange_common.EXCHANGE_ID_COINBASE_PRO: coinbase_pro.CoinbaseProDetails, + exchange_common.EXCHANGE_ID_TEST_EXCHANGE: testexchange.TestExchangeDetails, + exchange_common.EXCHANGE_ID_TEST_VOLATILE_EXCHANGE: test_volatile_exchange.TestVolatileExchangeDetails, + exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE: test_fixed_price_exchange.TestFixedPriceExchangeDetails, + } +) diff --git a/daemons/constants/static_exchange_query_config.go b/daemons/constants/static_exchange_query_config.go new file mode 100644 index 000000000..db9e45568 --- /dev/null +++ b/daemons/constants/static_exchange_query_config.go @@ -0,0 +1,137 @@ +package constants + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +const ( + defaultIntervalMs = 2_000 + // Binance / BinanceUS has a limit of 1_200 request weight per minute. At 40 request weight per + // iteration, we can query once every 2 seconds, but increase to 2.5s to allow for some jitter. + binanceIntervalMs = 2_500 + // Bitfinex has a rate limit of 30 requests per minute, so we query every 2.5 seconds + // to allow for some jitter, as the 2-second interval occasionally produces 429 responses. + bitfinexIntervalMs = 2_500 + defaultTimeoutMs = 3_000 + defaultMaxQueries = 3 + defaultMultiMarketMaxQueries = 1 +) + +var ( + StaticExchangeQueryConfig = map[types.ExchangeId]*types.ExchangeQueryConfig{ + // See above for rate limiting information of Binance. + // https://binance-docs.github.io/apidocs/spot/en/#24hr-ticker-price-change-statistics + exchange_common.EXCHANGE_ID_BINANCE: { + ExchangeId: exchange_common.EXCHANGE_ID_BINANCE, + IntervalMs: binanceIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // See above for rate limiting information of BinanceUS. + exchange_common.EXCHANGE_ID_BINANCE_US: { + ExchangeId: exchange_common.EXCHANGE_ID_BINANCE_US, + IntervalMs: binanceIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Bitfinex has a limit of 30 requests per minute. + // https://docs.bitfinex.com/reference/rest-public-tickers + exchange_common.EXCHANGE_ID_BITFINEX: { + ExchangeId: exchange_common.EXCHANGE_ID_BITFINEX, + IntervalMs: bitfinexIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + exchange_common.EXCHANGE_ID_KRAKEN: { + ExchangeId: exchange_common.EXCHANGE_ID_KRAKEN, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Gate has a limit of 900 requests/second + // https://www.gate.io/docs/developers/apiv4/en/#frequency-limit-rule + exchange_common.EXCHANGE_ID_GATE: { + ExchangeId: exchange_common.EXCHANGE_ID_GATE, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Bitstamp has a limit of 8000 requests per 10 minutes. + // https://www.bitstamp.net/api/#request-limits + exchange_common.EXCHANGE_ID_BITSTAMP: { + ExchangeId: exchange_common.EXCHANGE_ID_BITSTAMP, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Bybit has a limit of 120 requests per second for 5 consecutive seconds. + // https://bybit-exchange.github.io/docs/v5/rate-limit + exchange_common.EXCHANGE_ID_BYBIT: { + ExchangeId: exchange_common.EXCHANGE_ID_BYBIT, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Crypto.com has a limit of 100 requests per second. + // https://exchange-docs.crypto.com/derivatives/index.html#rate-limits + exchange_common.EXCHANGE_ID_CRYPTO_COM: { + ExchangeId: exchange_common.EXCHANGE_ID_CRYPTO_COM, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Huobi has a limit of 100 requests per second. + // https://huobiapi.github.io/docs/spot/v1/en/#api-access + exchange_common.EXCHANGE_ID_HUOBI: { + ExchangeId: exchange_common.EXCHANGE_ID_HUOBI, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Kucoin has a limit of 500 requests per 10 seconds. + // https://docs.kucoin.com/#request-rate-limit + exchange_common.EXCHANGE_ID_KUCOIN: { + ExchangeId: exchange_common.EXCHANGE_ID_KUCOIN, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Okx has a limit of 20 requests per 2 seconds. + // https://www.okx.com/docs-v5/en/#rest-api-market-data-get-tickers + exchange_common.EXCHANGE_ID_OKX: { + ExchangeId: exchange_common.EXCHANGE_ID_OKX, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // Mexc has a limit of 20 requests per second. + // https://mxcdevelop.github.io/apidocs/spot_v2_en/#rate-limit + exchange_common.EXCHANGE_ID_MEXC: { + ExchangeId: exchange_common.EXCHANGE_ID_MEXC, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMultiMarketMaxQueries, + }, + // CoinbasePro has a limit of 10 requests per second. + // https://docs.cloud.coinbase.com/exchange/docs/rest-rate-limits + exchange_common.EXCHANGE_ID_COINBASE_PRO: { + ExchangeId: exchange_common.EXCHANGE_ID_COINBASE_PRO, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMaxQueries, + }, + exchange_common.EXCHANGE_ID_TEST_VOLATILE_EXCHANGE: { + ExchangeId: exchange_common.EXCHANGE_ID_TEST_VOLATILE_EXCHANGE, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMaxQueries, + }, + exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE: { + ExchangeId: exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE, + IntervalMs: defaultIntervalMs, + TimeoutMs: defaultTimeoutMs, + MaxQueries: defaultMaxQueries, + }, + } +) diff --git a/daemons/constants/static_market_params_config.go b/daemons/constants/static_market_params_config.go new file mode 100644 index 000000000..6ff4b4905 --- /dev/null +++ b/daemons/constants/static_market_params_config.go @@ -0,0 +1,27 @@ +package constants + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + StaticMarketParamsConfig = map[uint32]*types.MarketParam{ + exchange_common.BTCUSD_ID: { + Id: exchange_common.BTCUSD_ID, + Pair: `"BTC-USD"`, + Exponent: -5, + MinExchanges: 1, + MinPriceChangePpm: 1000, + ExchangeConfigJson: `{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"BTCUSDT\\\"\"},{\"exchangeName\":\"BinanceUS\",\"ticker\":\"\\\"BTCUSD\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tBTCUSD\"},{\"exchangeName\":\"Bitstamp\",\"ticker\":\"BTC/USD\"},{\"exchangeName\":\"Bybit\",\"ticker\":\"BTCUSDT\"},{\"exchangeName\":\"CoinbasePro\",\"ticker\":\"BTC-USD\"},{\"exchangeName\":\"CryptoCom\",\"ticker\":\"BTC_USD\"},{\"exchangeName\":\"Kraken\",\"ticker\":\"XXBTZUSD\"},{\"exchangeName\":\"Okx\",\"ticker\":\"BTC-USDT\"}]}`, + }, + exchange_common.ETHUSD_ID: { + Id: exchange_common.ETHUSD_ID, + Pair: `"ETH-USD"`, + Exponent: -6, + MinExchanges: 1, + MinPriceChangePpm: 1000, + ExchangeConfigJson: `{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHUSDT\\\"\"},{\"exchangeName\":\"BinanceUS\",\"ticker\":\"\\\"ETHUSD\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHUSD\"},{\"exchangeName\":\"Bitstamp\",\"ticker\":\"ETH/USD\"},{\"exchangeName\":\"Bybit\",\"ticker\":\"ETHUSDT\"},{\"exchangeName\":\"CoinbasePro\",\"ticker\":\"ETH-USD\"},{\"exchangeName\":\"CryptoCom\",\"ticker\":\"ETH_USD\"},{\"exchangeName\":\"Kraken\",\"ticker\":\"XETHZUSD\"},{\"exchangeName\":\"Okx\",\"ticker\":\"ETH-USDT\"}]}`, + }, + } +) diff --git a/daemons/exchange_common/exchange_id.go b/daemons/exchange_common/exchange_id.go new file mode 100644 index 000000000..110b9c8f3 --- /dev/null +++ b/daemons/exchange_common/exchange_id.go @@ -0,0 +1,40 @@ +package exchange_common + +import ( + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +const ( + // EXCHANGE_ID_BINANCE is the id for Binance exchange. + EXCHANGE_ID_BINANCE types.ExchangeId = "Binance" + // EXCHANGE_ID_BINANCE_US is the id for BinanceUS exchange. + EXCHANGE_ID_BINANCE_US types.ExchangeId = "BinanceUS" + // EXCHANGE_ID_BITFINEX is the id for Bitfinex exchange. + EXCHANGE_ID_BITFINEX types.ExchangeId = "Bitfinex" + // EXCHANGE_ID_KRAKEN is the id for Kraken exchange + EXCHANGE_ID_KRAKEN types.ExchangeId = "Kraken" + // EXCHANGE_ID_GATE is the id for Gate exchange + EXCHANGE_ID_GATE types.ExchangeId = "Gate" + // EXCHANGE_ID_BITSTAMP is the id for Bitstamp exchange + EXCHANGE_ID_BITSTAMP types.ExchangeId = "Bitstamp" + // EXCHANGE_ID_BYBIT is the id for Bybit exchange + EXCHANGE_ID_BYBIT types.ExchangeId = "Bybit" + // EXCHANGE_ID_CRYPTO_COM is the id for CryptoCom exchange. + EXCHANGE_ID_CRYPTO_COM types.ExchangeId = "CryptoCom" + // EXCHANGE_ID_HUOBI is the id for Huobi exchange. + EXCHANGE_ID_HUOBI types.ExchangeId = "Huobi" + // EXCHANGE_ID_KUCOIN is the id for Kucoin exchange. + EXCHANGE_ID_KUCOIN types.ExchangeId = "Kucoin" + // EXCHANGE_ID_OKX is the id for Okx exchange. + EXCHANGE_ID_OKX types.ExchangeId = "Okx" + // EXCHANGE_ID_MEXC is the id for Mexc exchange. + EXCHANGE_ID_MEXC types.ExchangeId = "Mexc" + // EXCHANGE_ID_COINBASE_PRO is the id for CoinbasePro exchange. + EXCHANGE_ID_COINBASE_PRO types.ExchangeId = "CoinbasePro" + // EXCHANGE_ID_TEST_EXCHANGE is the id for test exchange. + EXCHANGE_ID_TEST_EXCHANGE types.ExchangeId = "TestExchange" + // EXCHANGE_ID_TEST_VOLATILE_EXCHANGE is the id for test volatile exchange. + EXCHANGE_ID_TEST_VOLATILE_EXCHANGE types.ExchangeId = "TestVolatileExchange" + // EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE is the id for test fixed price exchange. + EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE types.ExchangeId = "TestFixedPriceExchange" +) diff --git a/daemons/exchange_common/market_id.go b/daemons/exchange_common/market_id.go new file mode 100644 index 000000000..fed6a32f7 --- /dev/null +++ b/daemons/exchange_common/market_id.go @@ -0,0 +1,6 @@ +package exchange_common + +const ( + BTCUSD_ID uint32 = 0 + ETHUSD_ID uint32 = 1 +) diff --git a/daemons/flags/flags.go b/daemons/flags/flags.go new file mode 100644 index 000000000..e51033dbb --- /dev/null +++ b/daemons/flags/flags.go @@ -0,0 +1,141 @@ +package flags + +import ( + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/spf13/cast" + "github.com/spf13/cobra" +) + +// List of CLI flags for Server and Client. +const ( + // Flag names + FlagUnixSocketAddress = "unix-socket-address" + FlagPanicOnDaemonFailureEnabled = "panic-on-daemon-failure-enabled" + FlagMaxDaemonUnhealthySeconds = "max-daemon-unhealthy-seconds" + + FlagPriceDaemonEnabled = "price-daemon-enabled" + FlagPriceDaemonLoopDelayMs = "price-daemon-loop-delay-ms" +) + +// Shared flags contains configuration flags shared by all daemons. +type SharedFlags struct { + // SocketAddress is the location of the unix socket to communicate with the daemon gRPC service. + SocketAddress string + // PanicOnDaemonFailureEnabled toggles whether the daemon should panic on failure. + PanicOnDaemonFailureEnabled bool + // MaxDaemonUnhealthySeconds is the maximum allowable duration for which a daemon can be unhealthy. + MaxDaemonUnhealthySeconds uint32 +} + +// PriceFlags contains configuration flags for the Price Daemon. +type PriceFlags struct { + // Enabled toggles the price daemon on or off. + Enabled bool + // LoopDelayMs configures the update frequency of the price daemon. + LoopDelayMs uint32 +} + +// DaemonFlags contains the collected configuration flags for all daemons. +type DaemonFlags struct { + Shared SharedFlags + Price PriceFlags +} + +var defaultDaemonFlags *DaemonFlags + +// GetDefaultDaemonFlags returns the default values for the Daemon Flags using a singleton pattern. +func GetDefaultDaemonFlags() DaemonFlags { + if defaultDaemonFlags == nil { + defaultDaemonFlags = &DaemonFlags{ + Shared: SharedFlags{ + SocketAddress: "/tmp/daemons.sock", + PanicOnDaemonFailureEnabled: true, + MaxDaemonUnhealthySeconds: 5 * 60, // 5 minutes. + }, + Price: PriceFlags{ + Enabled: true, + LoopDelayMs: 3_000, + }, + } + } + return *defaultDaemonFlags +} + +// AddDaemonFlagsToCmd adds the required flags to instantiate a server and client for +// price updates. These flags should be applied to the `start` command LAYER Cosmos application. +// E.g. `layerd start --price-daemon-enabled=true --unix-socket-address $(unix_socket_address)` +func AddDaemonFlagsToCmd( + cmd *cobra.Command, +) { + // + df := GetDefaultDaemonFlags() + + // Shared Flags. + cmd.Flags().String( + FlagUnixSocketAddress, + df.Shared.SocketAddress, + "Socket address for the daemons to send updates to, if not set "+ + "will establish default location to ingest daemon updates from", + ) + cmd.Flags().Bool( + FlagPanicOnDaemonFailureEnabled, + df.Shared.PanicOnDaemonFailureEnabled, + "Enables panicking when a daemon fails.", + ) + cmd.Flags().Uint32( + FlagMaxDaemonUnhealthySeconds, + df.Shared.MaxDaemonUnhealthySeconds, + "Maximum allowable duration for which a daemon can be unhealthy.", + ) + + // Price Daemon. + cmd.Flags().Bool( + FlagPriceDaemonEnabled, + df.Price.Enabled, + "Enable Price Daemon. Set to false for non-validator nodes.", + ) + cmd.Flags().Uint32( + FlagPriceDaemonLoopDelayMs, + df.Price.LoopDelayMs, + "Delay in milliseconds between sending price updates to the application.", + ) +} + +// GetDaemonFlagValuesFromOptions gets all daemon flag values from the `AppOptions` struct. +func GetDaemonFlagValuesFromOptions( + appOpts servertypes.AppOptions, +) DaemonFlags { + // Default value + result := GetDefaultDaemonFlags() + + // Shared Flags + if option := appOpts.Get(FlagUnixSocketAddress); option != nil { + if v, err := cast.ToStringE(option); err == nil { + result.Shared.SocketAddress = v + } + } + if option := appOpts.Get(FlagPanicOnDaemonFailureEnabled); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.Shared.PanicOnDaemonFailureEnabled = v + } + } + if option := appOpts.Get(FlagMaxDaemonUnhealthySeconds); option != nil { + if v, err := cast.ToUint32E(option); err == nil { + result.Shared.MaxDaemonUnhealthySeconds = v + } + } + + // Price Daemon. + if option := appOpts.Get(FlagPriceDaemonEnabled); option != nil { + if v, err := cast.ToBoolE(option); err == nil { + result.Price.Enabled = v + } + } + if option := appOpts.Get(FlagPriceDaemonLoopDelayMs); option != nil { + if v, err := cast.ToUint32E(option); err == nil { + result.Price.LoopDelayMs = v + } + } + + return result +} diff --git a/daemons/flags/flags_test.go b/daemons/flags/flags_test.go new file mode 100644 index 000000000..1bddc45df --- /dev/null +++ b/daemons/flags/flags_test.go @@ -0,0 +1,77 @@ +package flags_test + +import ( + "fmt" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/flags" + "github.com/tellor-io/layer/mocks" +) + +func TestAddDaemonFlagsToCmd(t *testing.T) { + cmd := cobra.Command{} + + flags.AddDaemonFlagsToCmd(&cmd) + tests := []string{ + flags.FlagUnixSocketAddress, + flags.FlagPanicOnDaemonFailureEnabled, + flags.FlagMaxDaemonUnhealthySeconds, + + flags.FlagPriceDaemonEnabled, + flags.FlagPriceDaemonLoopDelayMs, + } + + for _, v := range tests { + testName := fmt.Sprintf("Has %s flag", v) + t.Run(testName, func(t *testing.T) { + require.Contains(t, cmd.Flags().FlagUsages(), v) + }) + } +} + +func TestGetDaemonFlagValuesFromOptions_Custom(t *testing.T) { + optsMap := make(map[string]interface{}) + + optsMap[flags.FlagUnixSocketAddress] = "test-socket-address" + optsMap[flags.FlagPanicOnDaemonFailureEnabled] = false + optsMap[flags.FlagMaxDaemonUnhealthySeconds] = uint32(1234) + + optsMap[flags.FlagPriceDaemonEnabled] = true + optsMap[flags.FlagPriceDaemonLoopDelayMs] = uint32(4444) + + mockOpts := mocks.AppOptions{} + mockOpts.On("Get", mock.Anything). + Return(func(key string) interface{} { + return optsMap[key] + }) + + r := flags.GetDaemonFlagValuesFromOptions(&mockOpts) + + // Shared. + require.Equal(t, optsMap[flags.FlagUnixSocketAddress], r.Shared.SocketAddress) + require.Equal(t, optsMap[flags.FlagPanicOnDaemonFailureEnabled], r.Shared.PanicOnDaemonFailureEnabled) + require.Equal( + t, + optsMap[flags.FlagMaxDaemonUnhealthySeconds], + r.Shared.MaxDaemonUnhealthySeconds, + ) + + // Price Daemon. + require.Equal(t, optsMap[flags.FlagPriceDaemonEnabled], r.Price.Enabled) + require.Equal(t, optsMap[flags.FlagPriceDaemonLoopDelayMs], r.Price.LoopDelayMs) +} + +func TestGetDaemonFlagValuesFromOptions_Default(t *testing.T) { + mockOpts := mocks.AppOptions{} + mockOpts.On("Get", mock.Anything). + Return(func(key string) interface{} { + return nil + }) + + r := flags.GetDaemonFlagValuesFromOptions(&mockOpts) + d := flags.GetDefaultDaemonFlags() + require.Equal(t, d, r) +} diff --git a/daemons/pricefeed/client/client.go b/daemons/pricefeed/client/client.go new file mode 100644 index 000000000..a409907af --- /dev/null +++ b/daemons/pricefeed/client/client.go @@ -0,0 +1,330 @@ +package client + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + appflags "github.com/tellor-io/layer/app/flags" + daemontypes "github.com/tellor-io/layer/daemons/types" + + sdklog "cosmossdk.io/log" + "github.com/cometbft/cometbft/libs/log" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/flags" + "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" + handler "github.com/tellor-io/layer/daemons/pricefeed/client/queryhandler" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + servertypes "github.com/tellor-io/layer/daemons/server/types" + libtime "github.com/tellor-io/layer/lib/time" +) + +// Client encapsulates the logic for executing and cleanly stopping all subtasks associated with the +// pricefeed client daemon. Access to the client's internal state is synchronized. +// The pricefeed daemon is a job that periodically queries external exchanges and transmits +// price data to the pricefeed service, which is then used by the application to compute index +// prices for proposing and validating oracle price updates on the blockchain. +// Note: price fetchers manage their own subtasks by blocking on their completion on every subtask run. +// When the price fetcher is stopped, it will wait for all of its own subtasks to complete before returning. +type Client struct { + // include HealthCheckable to track the health of the daemon. + daemontypes.HealthCheckable + + // daemonStartup tracks whether the daemon has finished startup. The daemon + // cannot be stopped until all persistent daemon subtasks have been launched within `Start`. + daemonStartup sync.WaitGroup + + // runningSubtasksWaitGroup tracks the number of running subtasks on the daemon. + // This is used to block the daemon from stopping until all running processes have completed. + runningSubtasksWaitGroup sync.WaitGroup + + // tickers tracks the list of tickers that are used to execute subtasks that repeat periodically on the daemon. + // Access to tickers is implicitly synchronized by the daemonStartup WaitGroup. + tickers []*time.Ticker + + // stops tracks the list of channels that are used to send a stop signal to subtasks on the daemon. + // Access to stops is implicitly synchronized by the daemonStartup WaitGroup. + stops []chan bool + + // Ensure stop only executes one time. + stopDaemon sync.Once + + // logger is the logger for the daemon. + logger log.Logger +} + +// Ensure Client implements the HealthCheckable interface. +var _ daemontypes.HealthCheckable = (*Client)(nil) + +func newClient(logger log.Logger) *Client { + logger = logger.With(sdklog.ModuleKey, constants.PricefeedDaemonModuleName) + client := &Client{ + tickers: []*time.Ticker{}, + stops: []chan bool{}, + HealthCheckable: daemontypes.NewTimeBoundedHealthCheckable( + constants.PricefeedDaemonModuleName, + &libtime.TimeProviderImpl{}, + logger, + ), + logger: logger, + } + + // Set the client's daemonStartup state to indicate that the daemon has not finished starting up. + client.daemonStartup.Add(1) + return client +} + +// newTickerWithStop creates a new ticker and a channel for iteratively looping through a subtask with a stop signal +// for any subtask kicked off by the client. The ticker and channel are tracked in order to properly clean up and send +// all needed stop signals when the daemon is stopped. +// Note: this method is not synchronized. It is expected to be called from the client's `StartNewClient` method before +// the daemonStartup waitgroup signals. +func (c *Client) newTickerWithStop(intervalMs int) (*time.Ticker, <-chan bool) { + ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond) + c.tickers = append(c.tickers, ticker) + + stop := make(chan bool) + c.stops = append(c.stops, stop) + + return ticker, stop +} + +// Stop stops the daemon and all running subtasks. This method is synchronized by the daemonStartup WaitGroup. +func (c *Client) Stop() { + c.stopDaemon.Do(func() { + c.daemonStartup.Wait() + + // Send a signal to all tickers and stop channels to stop all running subtasks managed by the client. + for _, stop := range c.stops { + close(stop) + } + for _, ticker := range c.tickers { + ticker.Stop() + } + + c.runningSubtasksWaitGroup.Wait() + }) +} + +// start begins a job that: +// A) periodically queries prices from external data sources and saves the retrieved prices in an +// in-memory datastore +// B) periodically sends the most recent prices to a gRPC server +// C) periodically queries the prices module for the latest market/exchange configuration and then updates +// the shared, in-memory datastore with the latest configuration. +// The exchangeIdToQueryConfig map dictates which exchanges the pricefeed client queries against. +// For all exchanges included in this map, the pricefeed client expects an exchangeQueryDetails and an +// initialExchangeMarketConfig object to be defined in the parameter maps. To initialize an exchange with +// zero markets, pass in an initialExchangeMarketConfig object with an empty map of market tickers for that +// exchange. +// Implementation: +// 1. Establish connections to gRPC servers. +// 2. Validate daemon configuration. +// 3. Initialize synchronized, in-memory shared daemon configuration. +// 4. Start PriceEncoder and PriceFetcher per exchange. Each price fetcher adds itself to the shared +// daemon config. +// 5. Start MarketUpdater subtask to periodically update the market configs. +// 6. Start PriceUpdater to begin broadcasting prices. +func (c *Client) start(ctx context.Context, + daemonFlags flags.DaemonFlags, + appFlags appflags.Flags, + grpcClient daemontypes.GrpcClient, + marketParams []types.MarketParam, + exchangeIdToQueryConfig map[types.ExchangeId]*types.ExchangeQueryConfig, + exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails, + subTaskRunner SubTaskRunner, +) (err error) { + // 1. Establish connections to gRPC servers. + queryConn, err := grpcClient.NewTcpConnection(ctx, appFlags.GrpcAddress) + if err != nil { + c.logger.Error("Failed to establish gRPC connection to Cosmos gRPC query services", "error", err) + return err + } + // Defer closing gRPC connection until job completes. + defer func() { + if connErr := grpcClient.CloseConnection(queryConn); connErr != nil { + err = connErr + } + }() + + daemonConn, err := grpcClient.NewGrpcConnection(ctx, daemonFlags.Shared.SocketAddress) + if err != nil { + c.logger.Error("Failed to establish gRPC connection to socket address", "error", err) + return err + } + // Defer closing gRPC connection until job completes. + defer func() { + if connErr := grpcClient.CloseConnection(daemonConn); connErr != nil { + err = connErr + } + }() + + // 2. Validate daemon configuration. + if err := validateDaemonConfiguration( + exchangeIdToQueryConfig, + exchangeIdToExchangeDetails, + ); err != nil { + return err + } + + // Let the canonical list of exchange feeds be the keys of the map of exchange feed ids to startup configs. + canonicalExchangeIds := make([]types.ExchangeId, 0, len(exchangeIdToQueryConfig)) + for exchangeId := range exchangeIdToQueryConfig { + canonicalExchangeIds = append(canonicalExchangeIds, exchangeId) + } + + // 3. Initialize synchronized, in-memory shared daemon configuration. + priceFeedMutableMarketConfigs := types.NewPriceFeedMutableMarketConfigs( + canonicalExchangeIds, + ) + + exchangeToMarketPrices, err := types.NewExchangeToMarketPrices(canonicalExchangeIds) + if err != nil { + return err + } + + // 4. Start PriceEncoder and PriceFetcher per exchange. + timeProvider := &libtime.TimeProviderImpl{} + for _exchangeId := range exchangeIdToQueryConfig { + // Assign these within the loop to avoid unexpected values being passed to the goroutines. + exchangeId := _exchangeId + exchangeConfig := exchangeIdToQueryConfig[exchangeId] + + // Expect an ExchangeQueryDetails to exist for each supported exchange feed id. + exchangeDetails, exists := exchangeIdToExchangeDetails[exchangeId] + if !exists { + return fmt.Errorf("no exchange details exists for exchangeId: %v", exchangeId) + } + + // Instantiate shared buffered channel to be written to by the price fetcher and read from + // by the price encoder. + bCh := make(chan *price_fetcher.PriceFetcherSubtaskResponse, constants.FixedBufferSize) + + c.runningSubtasksWaitGroup.Add(1) + go func() { + defer c.runningSubtasksWaitGroup.Done() + subTaskRunner.StartPriceEncoder( + exchangeId, + priceFeedMutableMarketConfigs, + exchangeToMarketPrices, + c.logger, + bCh, + ) + }() + + ticker, stop := c.newTickerWithStop(int(exchangeConfig.IntervalMs)) + c.runningSubtasksWaitGroup.Add(1) + go func() { + defer c.runningSubtasksWaitGroup.Done() + subTaskRunner.StartPriceFetcher( + ticker, + stop, + priceFeedMutableMarketConfigs, + *exchangeConfig, + exchangeDetails, + &handler.ExchangeQueryHandlerImpl{TimeProvider: timeProvider}, + c.logger, + bCh, + ) + }() + } + _, _ = priceFeedMutableMarketConfigs.UpdateMarkets(marketParams) + priceUpdaterTicker, priceUpdaterStop := c.newTickerWithStop(int(daemonFlags.Price.LoopDelayMs)) + // Now that all persistent subtasks have been started and all tickers and stop channels are created, + // signal that the startup process is complete. This needs to be called before entering the + // price updater loop, which loops indefinitely until the daemon is stopped. + c.daemonStartup.Done() + + pricefeedClient := servertypes.NewPriceFeedServiceClient(daemonConn) + subTaskRunner.StartPriceUpdater( + c, + ctx, + priceUpdaterTicker, + priceUpdaterStop, + exchangeToMarketPrices, + pricefeedClient, + c.logger, + ) + return nil +} + +// StartNewClient initializes and starts a new pricefeed daemon as a subtask of the calling process. +// The pricefeed daemon is a job that periodically queries external exchanges and transmits +// price data to the pricefeed service, which is then used by the application to compute index +// prices for proposing and validating oracle price updates on the blockchain. +// Note: the daemon will panic if it fails to start up. +func StartNewClient( + ctx context.Context, + daemonFlags flags.DaemonFlags, + appFlags appflags.Flags, + logger log.Logger, + grpcClient daemontypes.GrpcClient, + marketParams []types.MarketParam, + exchangeIdToQueryConfig map[types.ExchangeId]*types.ExchangeQueryConfig, + exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails, + subTaskRunner SubTaskRunner, +) (client *Client) { + // Log the daemon flags. + logger.Info( + "Starting pricefeed daemon with flags", + "PriceFlags", daemonFlags.Price, + ) + + client = newClient(logger) + client.runningSubtasksWaitGroup.Add(1) + go func() { + defer client.runningSubtasksWaitGroup.Done() + err := client.start( + ctx, + daemonFlags, + appFlags, + grpcClient, + marketParams, + exchangeIdToQueryConfig, + exchangeIdToExchangeDetails, + subTaskRunner, + ) + if err != nil { + logger.Error("Error initializing pricefeed daemon: %w", err.Error()) + panic(err) + } + }() + return client +} + +// validateDaemonConfiguration validates the daemon configuration. +// The list of exchanges used as keys for the exchangeIdToQueryConfig defines the exchanges used +// by the daemon. +// The daemon configuration is valid iff: +// 1) The exchangeIdToExchangeDetails map has an entry for each exchange. +// 2) The static exchange names map has an entry for each exchange, and each name is unique. +func validateDaemonConfiguration( + exchangeIdToQueryConfig map[types.ExchangeId]*types.ExchangeQueryConfig, + exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails, +) ( + err error, +) { + // Loop through all exchanges, which are defined by the exchangeIdToQueryConfig map, + // and validate all ids are unique and have a corresponding ExchangeQueryDetails. + exchangeIds := make(map[string]struct{}, len(exchangeIdToQueryConfig)) + for exchangeId := range exchangeIdToQueryConfig { + if _, exists := exchangeIds[exchangeId]; exists { + return fmt.Errorf("duplicate exchange id '%v' found for exchangeIds", exchangeId) + } + exchangeIds[exchangeId] = struct{}{} + + // Expect an ExchangeQueryDetails to exist for each supported exchange feed id. + if _, exists := exchangeIdToExchangeDetails[exchangeId]; !exists { + return fmt.Errorf("no exchange details exists for exchangeId: %v", exchangeId) + } + } + + // Validate that there is at least 1 exchange. + if len(exchangeIds) == 0 { + return errors.New("exchangeIds must not be empty") + } + + return nil +} diff --git a/daemons/pricefeed/client/price_encoder/adjust_by_market_details.go b/daemons/pricefeed/client/price_encoder/adjust_by_market_details.go new file mode 100644 index 000000000..8093a394d --- /dev/null +++ b/daemons/pricefeed/client/price_encoder/adjust_by_market_details.go @@ -0,0 +1,11 @@ +package price_encoder + +import "github.com/tellor-io/layer/daemons/pricefeed/client/types" + +// adjustByMarketDetails contains all information required to find and interpret the adjust-by market's price +// for the purposes of converting an exchange's raw API response price into a market price. +type adjustByMarketDetails struct { + MarketId types.MarketId + Exponent types.Exponent + MinExchanges uint32 +} diff --git a/daemons/pricefeed/client/price_encoder/mutable_state.go b/daemons/pricefeed/client/price_encoder/mutable_state.go new file mode 100644 index 000000000..278a5ad81 --- /dev/null +++ b/daemons/pricefeed/client/price_encoder/mutable_state.go @@ -0,0 +1,76 @@ +package price_encoder + +import ( + "fmt" + "sync" + + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// mutableState stores all mutable state for the price encoder. Unlike the price fetcher, the +// price encoder needs access to multiple parts of the mutable market state, so we store it all. +type mutableState struct { + sync.Mutex + mutableExchangeConfig *types.MutableExchangeMarketConfig + marketToMutableConfig map[types.MarketId]*types.MutableMarketConfig +} + +// Update updates all fields in the mutableState atomically. Updates are required +// to be atomic in order to keep the mutableState consistent. This method expects +// validation to occur in the PriceEncoder. This method is synchronized. +func (ms *mutableState) Update( + mutableExchangeConfig *types.MutableExchangeMarketConfig, + marketToMutableConfig map[types.MarketId]*types.MutableMarketConfig, +) { + ms.Lock() + defer ms.Unlock() + + ms.mutableExchangeConfig = mutableExchangeConfig + ms.marketToMutableConfig = marketToMutableConfig +} + +// GetPriceConversionDetailsForMarket returns the price conversion details for the given market. +func (ms *mutableState) GetPriceConversionDetailsForMarket(id types.MarketId) (priceConversionDetails, error) { + ms.Lock() + defer ms.Unlock() + + marketConfig, ok := ms.mutableExchangeConfig.MarketToMarketConfig[id] + if !ok { + return priceConversionDetails{}, fmt.Errorf( + "market config for market %v not found on exchange '%v'", + id, + ms.mutableExchangeConfig.Id, + ) + } + + var adjustDetails *adjustByMarketDetails + if marketConfig.AdjustByMarket != nil { + adjustByMarketConfig, ok := ms.marketToMutableConfig[*marketConfig.AdjustByMarket] + if !ok { + return priceConversionDetails{}, fmt.Errorf( + "mutable market config for adjust-by market %v not found on exchange '%v'", + *marketConfig.AdjustByMarket, + ms.mutableExchangeConfig.Id, + ) + } + adjustDetails = &adjustByMarketDetails{ + MarketId: *marketConfig.AdjustByMarket, + Exponent: adjustByMarketConfig.Exponent, + MinExchanges: adjustByMarketConfig.MinExchanges, + } + } + + mutableMarketConfig, ok := ms.marketToMutableConfig[id] + if !ok { + return priceConversionDetails{}, fmt.Errorf( + "mutable market config for market %v not found on exchange '%v'", + id, + ms.mutableExchangeConfig.Id, + ) + } + return priceConversionDetails{ + Invert: marketConfig.Invert, + Exponent: mutableMarketConfig.Exponent, + AdjustByMarketDetails: adjustDetails, + }, nil +} diff --git a/daemons/pricefeed/client/price_encoder/price_conversion_details.go b/daemons/pricefeed/client/price_encoder/price_conversion_details.go new file mode 100644 index 000000000..eee027d5e --- /dev/null +++ b/daemons/pricefeed/client/price_encoder/price_conversion_details.go @@ -0,0 +1,12 @@ +package price_encoder + +import "github.com/tellor-io/layer/daemons/pricefeed/client/types" + +// priceConversionDetails contains all information required to convert a ticker price from an exchange API response +// into a market price in the correct quote currency for the market (which, at this time, is uniformly USD.) +// If `adjustByMarketDetails` is nil, then the price is not adjusted by another market's price. +type priceConversionDetails struct { + Invert bool + Exponent types.Exponent + AdjustByMarketDetails *adjustByMarketDetails +} diff --git a/daemons/pricefeed/client/price_encoder/price_encoder.go b/daemons/pricefeed/client/price_encoder/price_encoder.go new file mode 100644 index 000000000..f0594fb26 --- /dev/null +++ b/daemons/pricefeed/client/price_encoder/price_encoder.go @@ -0,0 +1,405 @@ +package price_encoder + +import ( + "context" + "errors" + "fmt" + "syscall" + "time" + + gometrics "github.com/armon/go-metrics" + "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/lib/metrics" + "github.com/tellor-io/layer/lib/prices" +) + +type PriceEncoder interface { + types.ExchangeConfigUpdater + ProcessPriceFetcherResponse(response *price_fetcher.PriceFetcherSubtaskResponse) +} + +const ( + FailedToUpdateExchangePrice = "Failed to update exchange price in price daemon priceEncoder" +) + +// Enforce compile-time conformity of PriceEncoderImpl to the PriceEncoder interface. +var _ PriceEncoder = &PriceEncoderImpl{} + +type PriceEncoderImpl struct { + // isPastGracePeriod indicates the price encoder has passed the daemon startup grace period. Conversion failures + // are escalated to log errors after the grace period has passed. + isPastGracePeriod bool + exchangeId types.ExchangeId + exchangeToMarketPrices types.ExchangeToMarketPrices + logger log.Logger + bCh <-chan *price_fetcher.PriceFetcherSubtaskResponse + mutableState *mutableState +} + +// NewPriceEncoder creates a new, initialized PriceEncoderImpl struct. It manages decoding and converting +// of raw prices returned from the price fetcher into the shared exchangeToMarketPrices cache. All prices stored +// in the cache are converted to the market price in a quote currency of USD, even if the API request was made +// for a different market and/or used a different quote currency. +func NewPriceEncoder( + mutableExchangeConfig *types.MutableExchangeMarketConfig, + mutableMarketConfigs []*types.MutableMarketConfig, + exchangeToMarketPrices types.ExchangeToMarketPrices, + logger log.Logger, + bCh <-chan *price_fetcher.PriceFetcherSubtaskResponse, +) (*PriceEncoderImpl, error) { + pe := &PriceEncoderImpl{ + isPastGracePeriod: false, + exchangeId: mutableExchangeConfig.Id, + exchangeToMarketPrices: exchangeToMarketPrices, + logger: logger.With( + constants.SubmoduleLogKey, + constants.PriceEncoderSubmoduleName, + constants.ExchangeIdLogKey, + mutableExchangeConfig.Id, + ), + bCh: bCh, + mutableState: &mutableState{}, + } + + // Update mutable state. + err := pe.UpdateMutableExchangeConfig(mutableExchangeConfig, mutableMarketConfigs) + if err != nil { + return nil, err + } + + // Start background goroutine to toggle grace period after a delay. + go func() { + time.Sleep(constants.PriceDaemonStartupErrorGracePeriod) + pe.isPastGracePeriod = true + }() + + return pe, nil +} + +// UpdateMutableExchangeConfig updates the price encoder with the most current copy of the exchange config, as +// well as all markets supported by the exchange. +// This method is added to support the ExchangeConfigUpdater interface. +func (p *PriceEncoderImpl) UpdateMutableExchangeConfig( + newConfig *types.MutableExchangeMarketConfig, + newMarketConfigs []*types.MutableMarketConfig, +) error { + // 1. Validate new config. + if newConfig.Id != p.GetExchangeId() { + return fmt.Errorf( + "PriceEncoder.UpdateMutableExchangeConfig: exchange id mismatch, expected '%v', got '%v'", + p.GetExchangeId(), + newConfig.Id, + ) + } + + if err := newConfig.Validate(newMarketConfigs); err != nil { + return fmt.Errorf("PriceEncoder.UpdateMutableExchangeConfig: invalid exchange config update: %w", err) + } + + // 2. Derive price encoder mutable state. + newMarketsToMutableConfigs := make(map[types.MarketId]*types.MutableMarketConfig) + for _, newMarketConfig := range newMarketConfigs { + newMarketsToMutableConfigs[newMarketConfig.Id] = newMarketConfig + } + + // 3. Perform update. + p.mutableState.Update(newConfig, newMarketsToMutableConfigs) + return nil +} + +// GetExchangeId returns the exchange id for this PriceEncoder. +func (p *PriceEncoderImpl) GetExchangeId() types.ExchangeId { + return p.exchangeId +} + +// convertPriceUpdate converts a price update from the raw ticker price into a price for the market in the correct +// quote currency, drawing on the exchange config and market config to determine the correct conversion. +func (p PriceEncoderImpl) convertPriceUpdate(marketPriceTimestamp *types.MarketPriceTimestamp) ( + convertedPrice *types.MarketPriceTimestamp, + err error, +) { + conversionDetails, err := p.mutableState.GetPriceConversionDetailsForMarket(marketPriceTimestamp.MarketId) + if err != nil { + return nil, err + } + + // Create a logger with conversion details context for this method. + logger := p.logger.With( + "marketPriceTimestamp.Price", + marketPriceTimestamp.Price, + "marketExponent", + conversionDetails.Exponent, + constants.MarketIdLogKey, + marketPriceTimestamp.MarketId, + ) + + var price uint64 + if conversionDetails.AdjustByMarketDetails == nil { + if conversionDetails.Invert { + // price = 1 / marketPriceTimestamp.Price + price = prices.Invert(marketPriceTimestamp.Price, conversionDetails.Exponent) + logger.Debug("price_encoder: Inverting price without adjustment", constants.PriceLogKey, price) + } else { + // No adjustment or inversion required. + price = marketPriceTimestamp.Price + logger.Debug("price_encoder: Using price without adjustment or inversion", constants.PriceLogKey, price) + } + } else { + adjustByIndexPrice, numPricesMedianized := p.exchangeToMarketPrices.GetIndexPrice( + conversionDetails.AdjustByMarketDetails.MarketId, + time.Now().Add(-constants.MaxPriceAge), + lib.Median[uint64], + ) + // If the index price is not valid due to insufficient pricing data, return an error. + if numPricesMedianized < int(conversionDetails.AdjustByMarketDetails.MinExchanges) { + err = fmt.Errorf( + "Could not retrieve index price for market %v: "+ + "expected median price from %v exchanges, but got %v exchanges", + conversionDetails.AdjustByMarketDetails.MarketId, + conversionDetails.AdjustByMarketDetails.MinExchanges, + numPricesMedianized, + ) + return nil, err + } + + // Add adjustment market metadata to logger. + logger = logger.With( + "adjustByIndexPrice", + adjustByIndexPrice, + "adjustByExponent", + conversionDetails.AdjustByMarketDetails.Exponent, + ) + + if conversionDetails.Invert { + // price = adjustByIndexPrice / marketPriceTimestamp.Price + price = prices.Divide( + adjustByIndexPrice, + conversionDetails.AdjustByMarketDetails.Exponent, + marketPriceTimestamp.Price, + conversionDetails.Exponent, + ) + logger.Debug("price_encoder: Inverting price with adjustment", constants.PriceLogKey, price) + } else { + // marketPriceTimestamp.Price * adjustByIndexPrice + price = prices.Multiply( + marketPriceTimestamp.Price, + conversionDetails.Exponent, + adjustByIndexPrice, + conversionDetails.AdjustByMarketDetails.Exponent, + ) + logger.Debug("price_encoder: Multiplying price with adjustment", constants.PriceLogKey, price) + } + } + + // Emit market prices here for easy access to the market's exponent so that we can calculate the float32 + // representation of the price for metrics. If a price is available here, it will be put into the daemon prices + // cache by the encoder, and this is the earliest code location where a market's price is definitively resolved. + telemetry.SetGaugeWithLabels( + []string{metrics.PricefeedDaemon, metrics.PriceEncoderUpdatePrice}, + prices.PriceToFloat32ForLogging(price, conversionDetails.Exponent), + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(marketPriceTimestamp.MarketId), + pricefeedmetrics.GetLabelForExchangeId(p.GetExchangeId()), + }, + ) + + return &types.MarketPriceTimestamp{ + MarketId: marketPriceTimestamp.MarketId, + Price: price, + LastUpdatedAt: marketPriceTimestamp.LastUpdatedAt, + }, nil +} + +// UpdatePrice updates the price cache shared by the price updater with the converted market price. +func (p *PriceEncoderImpl) UpdatePrice(marketPriceTimestamp *types.MarketPriceTimestamp) { + // Convert price. + price, err := p.convertPriceUpdate(marketPriceTimestamp) + + if err != nil { + var logMethod = p.logger.Info + // When the price encoder starts, we expect that some conversions will fail as we are filling the cache with + // enough valid prices to generate a valid index price for our adjustment markets. In order to avoid spurious + // alerts, only emit error logs if the grace period has passed. + // There's a race condition here, and another one down below where we emit isPastGracePeriod as a log value, but + // that's ok. We don't need this to be perfect, we just need to avoid spurious alerts and have informative + // logs. + if p.isPastGracePeriod { + logMethod = p.logger.Error + } + logMethod( + "Failed to get price conversion details for market", + "error", + err, + constants.MarketIdLogKey, + marketPriceTimestamp.MarketId, + constants.ExchangeIdLogKey, + p.GetExchangeId(), + ) + // Record failure. + telemetry.IncrCounterWithLabels( + []string{metrics.PricefeedDaemon, metrics.PriceEncoderPriceConversion, metrics.Error}, + 1.0, + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(marketPriceTimestamp.MarketId), + pricefeedmetrics.GetLabelForExchangeId(p.GetExchangeId()), + }, + ) + return + } + + // Update exchangeToMarketPrices cache. + p.exchangeToMarketPrices.UpdatePrice(p.GetExchangeId(), price) + + // Record success. + telemetry.IncrCounterWithLabels( + []string{metrics.PricefeedDaemon, metrics.PriceEncoderPriceConversion, metrics.Success}, + 1.0, + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(marketPriceTimestamp.MarketId), + pricefeedmetrics.GetLabelForExchangeId(p.GetExchangeId()), + }, + ) +} + +// recordPriceUpdateExchangeFailure logs and reports metrics for exchange-related price update failures. +// These errors are logged at the info level so that there aren't noisy errors when undesirable but +// occasionally expected behavior occurs. +func recordPriceUpdateExchangeFailure( + reason string, + logger log.Logger, + err error, + exchangeId types.ExchangeId, +) { + logger.Info( + FailedToUpdateExchangePrice, + constants.ReasonLogKey, + reason, + constants.ExchangeIdLogKey, + exchangeId, + constants.ErrorLogKey, + err, + ) + + // Measure failure metric. + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedDaemon, + metrics.PriceEncoderUpdatePrice, + metrics.Exchange, + metrics.Error, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForExchangeId(exchangeId), + metrics.GetLabelForStringValue(metrics.Reason, reason), + }, + ) +} + +// ProcessPriceFetcherResponse consumes the (price, error) response from the price fetcher and either updates the +// exchangeToMarketPrices cache with a valid price, or appropriately logs and reports metrics for errors. +func (p *PriceEncoderImpl) ProcessPriceFetcherResponse(response *price_fetcher.PriceFetcherSubtaskResponse) { + // Capture nil response on channel close. + if response == nil { + panic("nil response received from price fetcher") + } + + // Capture exchange-specific errors. + var exchangeSpecificError price_function.ExchangeError + + if response.Err == nil { + p.UpdatePrice(response.Price) + } else { + if errors.Is(response.Err, context.DeadlineExceeded) { + // Log info if there are timeout errors in the ingested buffered channel prices. + recordPriceUpdateExchangeFailure( + metrics.HttpGetTimeout, + p.logger, + response.Err, + p.GetExchangeId(), + ) + } else if errors.Is(response.Err, constants.RateLimitingError) { + // Log an error if there are rate limiting errors in the ingested buffered channel prices. + p.logger.Error( + FailedToUpdateExchangePrice, + constants.ReasonLogKey, + metrics.RateLimit, + constants.ExchangeIdLogKey, + p.GetExchangeId(), + constants.ErrorLogKey, + response.Err, + ) + + // Measure failure metric. + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedDaemon, + metrics.PriceEncoderUpdatePrice, + metrics.Error, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForExchangeId(p.GetExchangeId()), + metrics.GetLabelForStringValue(metrics.Reason, metrics.RateLimit), + }, + ) + } else if ok := errors.As(response.Err, &exchangeSpecificError); ok { + // Log info if there are exchange-specific errors in the ingested buffered channel prices. + // These responses came back with an acceptable status code, but the response body contents + // were rejected by the price function as invalid. + recordPriceUpdateExchangeFailure( + metrics.ExchangeSpecificError, + p.logger, + response.Err, + p.GetExchangeId(), + ) + } else if price_function.IsGenericExchangeError(response.Err) { + // Log info if there are 5xx errors in the ingested buffered channel prices. These responses + // may have come back with an acceptable status code, but the response body contents indicate + // that the exchange is experiencing an internal error. + recordPriceUpdateExchangeFailure( + metrics.HttpGet5xx, + p.logger, + response.Err, + p.GetExchangeId(), + ) + } else if errors.Is(response.Err, syscall.ECONNRESET) { + // Log info if there are connections reset by the exchange. + recordPriceUpdateExchangeFailure( + metrics.HttpGetHangup, + p.logger, + response.Err, + p.GetExchangeId(), + ) + } else { + // Log error if there are errors in the ingested buffered channel prices. + p.logger.Error( + FailedToUpdateExchangePrice, + "error", + response.Err, + "exchangeId", + p.GetExchangeId(), + ) + + // Measure all failures in querying other than timeout. + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedDaemon, + metrics.PriceEncoderUpdatePrice, + metrics.Error, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForExchangeId(p.GetExchangeId()), + }, + ) + } + } +} diff --git a/daemons/pricefeed/client/price_fetcher/mutable_state.go b/daemons/pricefeed/client/price_fetcher/mutable_state.go new file mode 100644 index 000000000..4d223dcc5 --- /dev/null +++ b/daemons/pricefeed/client/price_fetcher/mutable_state.go @@ -0,0 +1,125 @@ +package price_fetcher + +import ( + "sync" + + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "golang.org/x/exp/maps" + "gopkg.in/typ.v4/lists" +) + +// mutableState stores the mutable state of the price fetcher. +// These parameters are updated periodically by a go routine that polls the pricefeed server +// for the current exchange and market configs. By keeping these parameters in a separate +// struct, we can better ensure that the price fetcher is respecting the synchronization +// requirements of accessing this data in order to stay in a consistent state. +// All access is synchronized by a RW mutex. +type mutableState struct { + // Market configuration for the price fetcher is synchronized as it is subject to change over time. + sync.Mutex + // Access to all following fields is protected. + // mutableExchangeConfig contains a copy of the current MutableExchangeMarketConfig for the exchange. + // It is updated periodically by the daemon's PriceFeedMutableMarketConfig when a change in the + // exchange's configuration is reported by the pricefeed server. + mutableExchangeConfig *types.MutableExchangeMarketConfig + // marketExponenets maps market ids to exponents for all markets supported by the exchange. + // It is updated every time the price fetcher's `UpdateMutableExchangeConfig` method is called. + marketExponents map[types.MarketId]types.Exponent + // marketIdsRing tracks the pointer to the next market to query for the upcoming task loop. + // It is replaced every time the price fetcher's `UpdateMutableExchangeConfig` method is called. + marketIdsRing *lists.Ring[types.MarketId] +} + +// GetMarketIds returns the current set of markets the price fetcher queries for this exchange. +// This method is synchronized. +func (ms *mutableState) GetMarketIds() []types.MarketId { + ms.Lock() + defer ms.Unlock() + + return ms.mutableExchangeConfig.GetMarketIds() +} + +// GetNextNMarkets returns the next n markets the price fetcher queries for this exchange, +// and advances the mutable state's ring of market ids so that the next call returns +// the next n markets. This method is synchronized. +func (ms *mutableState) GetNextNMarkets(n int) []types.MarketId { + // A writer lock is used here because the marketIdsRing pointer is advanced below. + ms.Lock() + defer ms.Unlock() + + markets := make([]types.MarketId, 0, n) + for i := 0; i < n; i++ { + markets = append(markets, ms.marketIdsRing.Value) + ms.marketIdsRing = ms.marketIdsRing.Next() + } + return markets +} + +// GetMarketExponents returns a copy of the current set of market exponents for this exchange. +// This method is synchronized. +func (ms *mutableState) GetMarketExponents() map[types.MarketId]types.Exponent { + ms.Lock() + defer ms.Unlock() + + return maps.Clone(ms.marketExponents) +} + +// GetMutableExchangeConfig returns a copy of the current MutableExchangeMarketConfig for the exchange. +// This method is synchronized. +func (ms *mutableState) GetMutableExchangeConfig() *types.MutableExchangeMarketConfig { + ms.Lock() + defer ms.Unlock() + + return ms.mutableExchangeConfig.Copy() +} + +// Update updates all fields in the mutableState atomically. Updates are required +// to be atomic in order to keep the mutableState consistent. This method expects +// validation to occur in the PriceFetcher. This method is synchronized. +func (ms *mutableState) Update( + config *types.MutableExchangeMarketConfig, + marketExponents map[types.MarketId]types.Exponent, + marketIdsRing *lists.Ring[types.MarketId], +) { + ms.Lock() + defer ms.Unlock() + + ms.mutableExchangeConfig = config + ms.marketExponents = marketExponents + ms.marketIdsRing = marketIdsRing +} + +// getTaskLoopDefinition returns a snapshot of the current price fetcher mutable state, while +// advancing the price fetcher's marketIdsRing to the next set of markets to query for +// single-market exchanges. +// This method is used to prevent R/W collisions on the price fetcher's mutable state from +// putting the task loop into an inconsistent state. +func (ms *mutableState) getTaskLoopDefinition( + isMultiMarket bool, + maxQueries int, +) ( + definition *taskLoopDefinition, +) { + ms.Lock() + defer ms.Unlock() + + // Compute which markets to query for this task loop. + // For single-market exchanges, we want to query the next set of markets in the ring. + var marketIds []types.MarketId + if isMultiMarket { + marketIds = ms.mutableExchangeConfig.GetMarketIds() + } else { + marketIds = make([]types.MarketId, 0, maxQueries) + for i := 0; i < maxQueries; i++ { + marketIds = append(marketIds, ms.marketIdsRing.Value) + ms.marketIdsRing = ms.marketIdsRing.Next() + } + } + + // Create a copy of all state to pass to the task loop. + return &taskLoopDefinition{ + mutableExchangeConfig: ms.mutableExchangeConfig.Copy(), + marketExponents: maps.Clone(ms.marketExponents), + marketIds: marketIds, + } +} diff --git a/daemons/pricefeed/client/price_fetcher/price_fetcher.go b/daemons/pricefeed/client/price_fetcher/price_fetcher.go new file mode 100644 index 000000000..14e3a709d --- /dev/null +++ b/daemons/pricefeed/client/price_fetcher/price_fetcher.go @@ -0,0 +1,344 @@ +package price_fetcher + +import ( + "context" + "fmt" + "math/rand" + "sync" + "time" + + "github.com/cosmos/cosmos-sdk/telemetry" + daemontypes "github.com/tellor-io/layer/daemons/types" + + gometrics "github.com/armon/go-metrics" + "github.com/cometbft/cometbft/libs/log" + "github.com/tellor-io/layer/daemons/constants" + handler "github.com/tellor-io/layer/daemons/pricefeed/client/queryhandler" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/lib/metrics" + "gopkg.in/typ.v4/lists" +) + +// PriceFetcherSubtaskResponse represents a transformed exchange API response that contains price +// info or an error. +type PriceFetcherSubtaskResponse struct { + Price *types.MarketPriceTimestamp + Err error +} + +// PriceFetcher fetches prices from an exchange by making a query based on the +// `exchangeConfig` specifications and then encodes the price or any associated error. +type PriceFetcher struct { + exchangeQueryConfig types.ExchangeQueryConfig + exchangeDetails types.ExchangeQueryDetails + queryHandler handler.ExchangeQueryHandler + logger log.Logger + bCh chan<- *PriceFetcherSubtaskResponse + + // mutableState contains all mutable state on the price fetcher is consolidated into a single object with access + // and update protected by a mutex. + mutableState *mutableState +} + +// NewPriceFetcher creates a new PriceFetcher struct. It manages querying markets via goroutine +// queries to an exchange and encodes the responses or related errors into the shared buffered +// channel `bCh`. +func NewPriceFetcher( + exchangeQueryConfig types.ExchangeQueryConfig, + exchangeDetails types.ExchangeQueryDetails, + mutableExchangeConfig *types.MutableExchangeMarketConfig, + mutableMarketConfigs []*types.MutableMarketConfig, + queryHandler handler.ExchangeQueryHandler, + logger log.Logger, + bCh chan<- *PriceFetcherSubtaskResponse, +) ( + *PriceFetcher, + error, +) { + // Configure price fetcher logger to have fetcher-specific metadata. + pfLogger := logger.With( + constants.SubmoduleLogKey, + constants.PriceFetcherSubmoduleName, + constants.ExchangeIdLogKey, + exchangeQueryConfig.ExchangeId, + ) + + pf := &PriceFetcher{ + exchangeQueryConfig: exchangeQueryConfig, + exchangeDetails: exchangeDetails, + queryHandler: queryHandler, + logger: pfLogger, + bCh: bCh, + mutableState: &mutableState{}, + } + + // This will instantiate the price fetcher's mutable state. + err := pf.UpdateMutableExchangeConfig(mutableExchangeConfig, mutableMarketConfigs) + if err != nil { + return nil, err + } + + return pf, nil +} + +// GetExchangeId returns the exchange id for the exchange queried by the price fetcher. +// This method is added to support the MutableExchangeConfigUpdater interface. +func (p *PriceFetcher) GetExchangeId() types.ExchangeId { + return p.exchangeQueryConfig.ExchangeId +} + +// UpdateMutableExchangeConfig updates the price fetcher with the most current copy of the exchange config, as +// well as all markets supported by the exchange. +// This method is added to support the ExchangeConfigUpdater interface. +func (p *PriceFetcher) UpdateMutableExchangeConfig( + newConfig *types.MutableExchangeMarketConfig, + newMarketConfigs []*types.MutableMarketConfig, +) error { + // 1. Validate new config. + if newConfig.Id != p.exchangeQueryConfig.ExchangeId { + return fmt.Errorf("PriceFetcher.UpdateMutableExchangeConfig: exchange id mismatch") + } + + if err := newConfig.Validate(newMarketConfigs); err != nil { + return fmt.Errorf("PriceFetcher.UpdateMutableExchangeConfig: invalid exchange config update: %w", err) + } + + // 2. Derive price fetcher mutable state. + // 2.A Compute market exponents. + marketExponents := make(map[types.MarketId]types.Exponent, len(newMarketConfigs)) + for _, marketConfig := range newMarketConfigs { + marketExponents[marketConfig.Id] = marketConfig.Exponent + } + + // 2.B Compute market ids ring. + marketIdsRing := lists.NewRing[types.MarketId](len(newConfig.GetMarketIds())) + for _, marketId := range newConfig.GetMarketIds() { + marketIdsRing.Value = marketId + marketIdsRing = marketIdsRing.Next() + } + + // 3. Perform update. + p.mutableState.Update(newConfig, marketExponents, marketIdsRing) + return nil +} + +// getTaskLoopDefinition returns a snapshot of the current price fetcher mutable state. +func (p *PriceFetcher) getTaskLoopDefinition() *taskLoopDefinition { + return p.mutableState.getTaskLoopDefinition( + p.exchangeDetails.IsMultiMarket, + p.getNumQueriesPerTaskLoop(), + ) +} + +// isMultiMarketAndHasMarkets returns true if the price fetcher is a multi-market fetcher +// and is currently configured to query for 1 or more markets. In this case, the fetcher +// should execute a single subtask query for all markets. For multi-market exchanges, this +// will still be false if the price fetcher has no supported markets. +func (pf *PriceFetcher) isMultiMarketAndHasMarkets() bool { + return pf.exchangeDetails.IsMultiMarket && len(pf.mutableState.GetMarketIds()) > 0 +} + +// getNumQueriesPerTaskLoop returns the number of queries that the price fetcher should execute +// on each task loop execution. For multi-market exchanges, this will always be 1. +// Otherwise, it will be the minimum of the number of markets supported by the exchange and +// the query limit specified in the exchange config to prevent exceeding the exchange's rate +// limit. +func (p *PriceFetcher) getNumQueriesPerTaskLoop() int { + if p.exchangeDetails.IsMultiMarket { + return 1 + } + return lib.Min( + int(p.exchangeQueryConfig.MaxQueries), + len(p.mutableState.GetMarketIds()), + ) +} + +// RunTaskLoop queries the exchange for market prices. +// Each goroutine makes a single exchange query for a specific set of one or more markets. +// RunTaskLoop blocks until all spawned goroutines have completed. +func (pf *PriceFetcher) RunTaskLoop(requestHandler daemontypes.RequestHandler) { + taskLoopDefinition := pf.getTaskLoopDefinition() + + if pf.isMultiMarketAndHasMarkets() { + pf.runSubTask( + requestHandler, + taskLoopDefinition.marketIds, + taskLoopDefinition, + ) + } else { + // Run all subtasks in parallel and wait for each to complete. + var waitGroup sync.WaitGroup + for i := 0; i < len(taskLoopDefinition.marketIds); i++ { + market := taskLoopDefinition.marketIds[i] + waitGroup.Add(1) + go func() { + defer waitGroup.Done() + pf.runSubTask( + requestHandler, + []types.MarketId{market}, + taskLoopDefinition, + ) + }() + } + waitGroup.Wait() + } +} + +// emitMarketAvailabilityMetrics emits telemetry that tracks whether a market was available when queried on an exchange. +// Success is tracked by (market, exchange) so that we can track the availability of each market on each exchange. +func emitMarketAvailabilityMetrics(exchangeId types.ExchangeId, id types.MarketId, available bool) { + success := metrics.Success + if !available { + success = metrics.Error + } + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedDaemon, + metrics.PriceFetcherQueryForMarket, + success, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForExchangeId(exchangeId), + pricefeedmetrics.GetLabelForMarketId(id), + }, + ) +} + +// runSubTask makes a single query to an exchange for market prices. This query can be for 1 or +// n markets. +// For single market exchanges, a task loop execution will execute multiple runSubTask goroutines, where +// each goroutine will query for a single market. To support this, we explicitly define the set of markets +// to query for in the `marketIds` parameter, even though in some cases it may be redundantly defined on +// the taskLoopDefinition. For multi-market exchanges, the taskLoop will execute exactly one subtask, and +// that subtask will query all markets defined in the taskLoopDefinition. +func (pf *PriceFetcher) runSubTask( + requestHandler daemontypes.RequestHandler, + marketIds []types.MarketId, + taskLoopDefinition *taskLoopDefinition, +) { + exchangeId := pf.exchangeQueryConfig.ExchangeId + + // Measure total latency for subtask to run for one API call and creating a context with timeout. + defer metrics.ModuleMeasureSinceWithLabels( + metrics.PricefeedDaemon, + []string{ + metrics.PricefeedDaemon, + metrics.PriceFetcherSubtaskLoopAndSetCtxTimeout, + metrics.Latency, + }, + time.Now(), + []gometrics.Label{pricefeedmetrics.GetLabelForExchangeId(exchangeId)}, + ) + + ctxWithTimeout, cancelFunc := context.WithTimeout( + context.Background(), + time.Duration(pf.exchangeQueryConfig.TimeoutMs)*time.Millisecond, + ) + + defer cancelFunc() + + // Measure total latency for subtask to run for one API call. + defer metrics.ModuleMeasureSinceWithLabels( + metrics.PricefeedDaemon, + []string{ + metrics.PricefeedDaemon, + metrics.PriceFetcherSubtaskLoop, + metrics.Latency, + }, + time.Now(), + []gometrics.Label{pricefeedmetrics.GetLabelForExchangeId(exchangeId)}, + ) + + prices, _, err := pf.queryHandler.Query( + ctxWithTimeout, + &pf.exchangeDetails, + taskLoopDefinition.mutableExchangeConfig, + marketIds, + requestHandler, + taskLoopDefinition.marketExponents, + ) + + // Emit metrics at the `AvailableMarketsSampleRate`. + emitMetricsSample := rand.Float64() < metrics.AvailableMarketsSampleRate + + if err != nil { + pf.writeToBufferedChannel(exchangeId, nil, err) + + // Since the query failed, report all markets as unavailable, according to the sampling rate. + if emitMetricsSample { + for _, marketId := range marketIds { + emitMarketAvailabilityMetrics(exchangeId, marketId, false) + } + } + + return + } + + // Track which markets were available when queried, and which were not, for telemetry. + availableMarkets := make(map[types.MarketId]bool, len(marketIds)) + for _, marketId := range marketIds { + availableMarkets[marketId] = false + } + + for _, price := range prices { + // No price should validly be zero. A price of zero points to an error in the API queried. + if price.Price == uint64(0) { + pf.writeToBufferedChannel( + exchangeId, + nil, + fmt.Errorf( + "Invalid price of 0 for exchange: '%v' and market: %v", + exchangeId, + price.MarketId, + ), + ) + + continue + } + + // Log each new price (per-market per-exchange). + pf.logger.Info( + "price_fetcher: Adding new price for market.", + constants.PriceLogKey, + price.Price, + constants.MarketIdLogKey, + price.MarketId, + "LastUpdatedAt", + price.LastUpdatedAt, + ) + + // Report market as available. + availableMarkets[price.MarketId] = true + + pf.writeToBufferedChannel(exchangeId, price, err) + } + + // Emit metrics on this exchange's market availability according to the sampling rate. + if emitMetricsSample { + for marketId, available := range availableMarkets { + emitMarketAvailabilityMetrics(exchangeId, marketId, available) + } + } +} + +// writeToBufferedChannel writes the (price, error) generated during querying to the price fetcher's +// buffered channel, which outputs the query result to the price encoder. +func (pf *PriceFetcher) writeToBufferedChannel( + exchangeId types.ExchangeId, + price *types.MarketPriceTimestamp, + err error, +) { + // Sanity check that the channel is not full already. + if len(pf.bCh) == constants.FixedBufferSize { + // Log if writing to buffered channel failed. + pf.logger.Error("Pricefeed daemon's shared buffer is full.") + } + + pf.bCh <- &PriceFetcherSubtaskResponse{ + Err: err, + Price: price, + } +} diff --git a/daemons/pricefeed/client/price_fetcher/task_loop_definition.go b/daemons/pricefeed/client/price_fetcher/task_loop_definition.go new file mode 100644 index 000000000..db7706f19 --- /dev/null +++ b/daemons/pricefeed/client/price_fetcher/task_loop_definition.go @@ -0,0 +1,22 @@ +package price_fetcher + +import "github.com/tellor-io/layer/daemons/pricefeed/client/types" + +// taskLoopDefinition defines the parameters for the price fetcher's task loop: +// Which markets to query, and parameters for how to query them and process query results. +// It's mostly identical to the price fetcher's mutable state, except that the market ids +// to query for the next task loop are articulated explicitly. This struct is used to pass +// snapshot of the current parameters to the price fetcher's task loop in order to prevent +// writes to the price fetcher's mutable state from putting the task loop into an +// inconsistent state whenever the price fetcher's mutable state is updated. +// +// This was judged as a better alternative than locking the price fetcher's +// mutable state for the duration of the task loop so that it would not create any blocking +// issues for the go routine that fetches config updates from the pricefeed server. If the +// price fetcher's mutable state updates in the middle of a task loop execution, it will be +// ignored by that loop and picked up by the next one. +type taskLoopDefinition struct { + mutableExchangeConfig *types.MutableExchangeMarketConfig + marketExponents map[types.MarketId]types.Exponent + marketIds []types.MarketId +} diff --git a/daemons/pricefeed/client/queryhandler/exchange_query_handler.go b/daemons/pricefeed/client/queryhandler/exchange_query_handler.go new file mode 100644 index 000000000..d78ea8273 --- /dev/null +++ b/daemons/pricefeed/client/queryhandler/exchange_query_handler.go @@ -0,0 +1,149 @@ +package handler + +import ( + "context" + "errors" + "fmt" + "strings" + + daemontypes "github.com/tellor-io/layer/daemons/types" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + clienttypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/lib" + libtime "github.com/tellor-io/layer/lib/time" +) + +const ( + UnexpectedResponseStatusMessage = "Unexpected response status code of:" +) + +var ( + RateLimitingError = fmt.Errorf("status 429 - rate limit exceeded") +) + +// ExchangeQueryHandlerImpl is the struct that implements the `ExchangeQueryHandler` interface. +type ExchangeQueryHandlerImpl struct { + libtime.TimeProvider +} + +// Ensure the `ExchangeQueryHandlerImpl` struct is implemented at compile time +var _ ExchangeQueryHandler = (*ExchangeQueryHandlerImpl)(nil) + +// ExchangeQueryHandler is an interface that encapsulates querying an exchange for price info. +type ExchangeQueryHandler interface { + libtime.TimeProvider + Query( + ctx context.Context, + exchangeQueryDetails *clienttypes.ExchangeQueryDetails, + exchangeConfig *clienttypes.MutableExchangeMarketConfig, + marketIds []clienttypes.MarketId, + requestHandler daemontypes.RequestHandler, + marketPriceExponent map[clienttypes.MarketId]clienttypes.Exponent, + ) (marketPriceTimestamps []*clienttypes.MarketPriceTimestamp, unavailableMarkets map[clienttypes.MarketId]error, err error) +} + +// Query makes an API call to a specific exchange and returns the transformed response, including both valid prices +// and any unavailable markets with specific errors. +// 1) Validate `marketIds` contains at least one id. +// 2) Convert the list of `marketIds` to tickers that are specific for a given exchange. Create a mapping of +// tickers to price exponents and a reverse mapping of ticker back to `MarketId`. +// 3) Make API call to an exchange and verify the response status code is not an error status code. +// 4) Transform the API response to market prices, while tracking unavailable tickers. +// 5) Return dual values: +// - a slice of `MarketPriceTimestamp`s that contains resolved market prices +// - a map of marketIds that could not be resolved with corresponding specific errors. +func (eqh *ExchangeQueryHandlerImpl) Query( + ctx context.Context, + exchangeQueryDetails *clienttypes.ExchangeQueryDetails, + exchangeConfig *clienttypes.MutableExchangeMarketConfig, + marketIds []clienttypes.MarketId, + requestHandler daemontypes.RequestHandler, + marketPriceExponent map[clienttypes.MarketId]clienttypes.Exponent, +) (marketPriceTimestamps []*clienttypes.MarketPriceTimestamp, unavailableMarkets map[clienttypes.MarketId]error, err error) { + // 1) Validate `marketIds` contains at least one id. + if len(marketIds) == 0 { + return nil, nil, errors.New("At least one marketId must be queried") + } + // 2) Convert the list of `marketIds` to tickers that are specific for a given exchange. Create a mapping + // of tickers to price exponents and a reverse mapping of ticker back to `MarketId`. + tickers := make([]string, 0, len(marketIds)) + tickerToPriceExponent := make(map[string]int32, len(marketIds)) + tickerToMarketId := make(map[string]clienttypes.MarketId, len(marketIds)) + for _, marketId := range marketIds { + config, ok := exchangeConfig.MarketToMarketConfig[marketId] + if !ok { + return nil, nil, fmt.Errorf("No market config for market: %v", marketId) + } + priceExponent, ok := marketPriceExponent[marketId] + if !ok { + return nil, nil, fmt.Errorf("No market price exponent for id: %v", marketId) + } + + tickers = append(tickers, config.Ticker) + tickerToPriceExponent[config.Ticker] = priceExponent + tickerToMarketId[config.Ticker] = marketId + + } + + // 3) Make API call to an exchange and verify the response status code is not an error status code. + url := CreateRequestUrl(exchangeQueryDetails.Url, tickers) + response, err := requestHandler.Get(ctx, url) + if err != nil { + return nil, nil, err + } + + if response.StatusCode == 429 { + return nil, nil, RateLimitingError + } + + // Verify response is not 4xx or 5xx. + if response.StatusCode < 200 || response.StatusCode > 299 { + return nil, nil, fmt.Errorf("%s %v", UnexpectedResponseStatusMessage, response.StatusCode) + } + + // 4) Transform the API response to market prices, while tracking unavailable tickers. + prices, unavailableTickers, err := exchangeQueryDetails.PriceFunction( + response, + tickerToPriceExponent, + lib.Median[uint64], + ) + if err != nil { + return nil, nil, price_function.NewExchangeError(exchangeQueryDetails.Exchange, err.Error()) + } + + // 5) Insert prices into MarketPriceTimestamp struct slice, convert unavailable tickers back into marketIds, + // and return. + marketPriceTimestamps = make([]*clienttypes.MarketPriceTimestamp, 0, len(prices)) + now := eqh.Now() + + for ticker, price := range prices { + marketId, ok := tickerToMarketId[ticker] + if !ok { + return nil, nil, fmt.Errorf("Severe unexpected error: no market id for ticker: %v", ticker) + } + + marketPriceTimestamp := &clienttypes.MarketPriceTimestamp{ + MarketId: marketId, + Price: price, + LastUpdatedAt: now, + } + + marketPriceTimestamps = append(marketPriceTimestamps, marketPriceTimestamp) + } + + unavailableMarkets = make(map[clienttypes.MarketId]error, len(unavailableTickers)) + for ticker, error := range unavailableTickers { + marketId, ok := tickerToMarketId[ticker] + if !ok { + return nil, nil, fmt.Errorf("Severe unexpected error: no market id for ticker: %v", ticker) + } + unavailableMarkets[marketId] = error + } + + return marketPriceTimestamps, unavailableMarkets, nil +} + +func CreateRequestUrl(baseUrl string, tickers []string) string { + return strings.Replace(baseUrl, "$", strings.Join(tickers, ","), -1) +} diff --git a/daemons/pricefeed/client/sources/binance/binance.go b/daemons/pricefeed/client/sources/binance/binance.go new file mode 100644 index 000000000..f49116bfe --- /dev/null +++ b/daemons/pricefeed/client/sources/binance/binance.go @@ -0,0 +1,59 @@ +package binance + +import ( + "encoding/json" + "net/http" + + "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// BinanceTicker is our representation of ticker information returned in Binance response. +// It implements interface `Ticker` in util.go. +type BinanceTicker struct { + Pair string `json:"symbol" validate:"required"` + AskPrice string `json:"askPrice" validate:"required,positive-float-string"` + BidPrice string `json:"bidPrice" validate:"required,positive-float-string"` + LastPrice string `json:"lastPrice" validate:"required,positive-float-string"` +} + +// Ensure that BinanceTicker implements the Ticker interface at compile time. +var _ sources.Ticker = (*BinanceTicker)(nil) + +func (t BinanceTicker) GetPair() string { + // needs to be wrapped in quotes to be consistent with the API request format. + return t.Pair +} + +func (t BinanceTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t BinanceTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t BinanceTicker) GetLastPrice() string { + return t.LastPrice +} + +// BinancePriceFunction transforms an API response from Binance into a map of tickers to prices that have been +// shifted by a market specific exponent. +func BinancePriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body into a list of tickers. + var binanceTickers []BinanceTicker + err = json.NewDecoder(response.Body).Decode(&binanceTickers) + if err != nil { + return nil, nil, err + } + + return sources.GetMedianPricesFromTickers( + binanceTickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/binance/binance_test.go b/daemons/pricefeed/client/sources/binance/binance_test.go new file mode 100644 index 000000000..62550159f --- /dev/null +++ b/daemons/pricefeed/client/sources/binance/binance_test.go @@ -0,0 +1,231 @@ +package binance_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/binance" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Binance. +const ( + BTCUSDC_TICKER = "BTCUSDT" + ETHUSDC_TICKER = "ETHUSDT" +) + +// Test exponent maps. +var ( + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestBinancePriceFunction_Mixed(t *testing.T) { + // Test response strings. + var ( + btcTicker = pricefeed.ReadJsonTestFile(t, "btc_ticker_binance.json") + ethTicker = pricefeed.ReadJsonTestFile(t, "eth_ticker_binance.json") + + ResponseStringTemplate = `[%s]` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`[%s,%s]`, btcTicker, ethTicker) + ) + + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780.29000000","bidPrice":"1780.24000000","askPrice":"1780.25000000",}`), + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to number askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780.29000000","bidPrice":"1780.24000000","askPrice":1780.25000000}`), + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "BinanceTicker.askPrice of type string"), + }, + "Unavailable - bid price is 0": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780.29000000","bidPrice":"0","askPrice":"1780.25000000"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'BinanceTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - ask price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780.29000000","bidPrice":"1780.24000000","askPrice":"-1780.25000000"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'BinanceTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - last price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"-1780.29000000","bidPrice":"1780.24000000","askPrice":"1780.25000000"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'BinanceTicker.LastPrice' Error:Field validation for " + + "'LastPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTCUSDT"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTCUSDT"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780.29000000","askPrice":"1780.25000000"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'BinanceTicker.BidPrice' Error:Field validation for 'BidPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Mixed - missing btc response and has eth response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_780_250_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTCUSDT"), + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780","bidPrice":"1780","askPrice":"1780.25000000"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_780_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_794_470_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","lastPrice":"1780.2900","bidPrice":"1780.240","askPrice":"1780.25123752942"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_780_251_237), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_794), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_794_470_000), + ETHUSDC_TICKER: uint64(1_780_250_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_780_250_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = binance.BinancePriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = binance.BinancePriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/binance/exchange_query_details.go b/daemons/pricefeed/client/sources/binance/exchange_query_details.go new file mode 100644 index 000000000..a1512985d --- /dev/null +++ b/daemons/pricefeed/client/sources/binance/exchange_query_details.go @@ -0,0 +1,22 @@ +package binance + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + BinanceDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_BINANCE, + Url: "https://data-api.binance.vision/api/v3/ticker/24hr", + PriceFunction: BinancePriceFunction, + IsMultiMarket: true, + } + + BinanceUSDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_BINANCE_US, + Url: "https://api.binance.us/api/v3/ticker/24hr", + PriceFunction: BinancePriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/binance/exchange_query_details_test.go b/daemons/pricefeed/client/sources/binance/exchange_query_details_test.go new file mode 100644 index 000000000..06e1600e3 --- /dev/null +++ b/daemons/pricefeed/client/sources/binance/exchange_query_details_test.go @@ -0,0 +1,24 @@ +package binance_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/binance" +) + +func TestBinanceUrl(t *testing.T) { + require.Equal(t, "https://data-api.binance.vision/api/v3/ticker/24hr", binance.BinanceDetails.Url) +} + +func TestBinanceUsUrl(t *testing.T) { + require.Equal(t, "https://api.binance.us/api/v3/ticker/24hr", binance.BinanceUSDetails.Url) +} + +func TestBinanceIsMultiMarket(t *testing.T) { + require.True(t, binance.BinanceDetails.IsMultiMarket) +} + +func TestBinanceUSIsMultiMarket(t *testing.T) { + require.True(t, binance.BinanceUSDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/binance/testdata/btc_ticker_binance.json b/daemons/pricefeed/client/sources/binance/testdata/btc_ticker_binance.json new file mode 100644 index 000000000..892aa9cde --- /dev/null +++ b/daemons/pricefeed/client/sources/binance/testdata/btc_ticker_binance.json @@ -0,0 +1,23 @@ +{ + "symbol":"BTCUSDT", + "priceChange":"1173.08000000", + "priceChangePercent":"4.382", + "weightedAvgPrice":"27276.80501741", + "prevClosePrice":"26771.61000000", + "lastPrice":"27944.70000000", + "lastQty":"0.54273000", + "bidPrice":"27944.70000000", + "bidQty":"1.41017000", + "askPrice":"27944.71000000", + "askQty":"1.67637000", + "openPrice":"26771.62000000", + "highPrice":"28140.00000000", + "lowPrice":"26652.00000000", + "volume":"62285.78422000", + "quoteVolume":"1698957191.52552460", + "openTime":1687206042695, + "closeTime":1687292442695, + "firstId":3148265489, + "lastId":3149346784, + "count":1081296 +} diff --git a/daemons/pricefeed/client/sources/binance/testdata/eth_ticker_binance.json b/daemons/pricefeed/client/sources/binance/testdata/eth_ticker_binance.json new file mode 100644 index 000000000..5a476e4cf --- /dev/null +++ b/daemons/pricefeed/client/sources/binance/testdata/eth_ticker_binance.json @@ -0,0 +1,23 @@ +{ + "symbol":"ETHUSDT", + "priceChange":"49.66000000", + "priceChangePercent":"2.869", + "weightedAvgPrice":"1749.46550291", + "prevClosePrice":"1730.64000000", + "lastPrice":"1780.29000000", + "lastQty":"9.09650000", + "bidPrice":"1780.24000000", + "bidQty":"3.50680000", + "askPrice":"1780.25000000", + "askQty":"48.71310000", + "openPrice":"1730.63000000", + "highPrice":"1791.88000000", + "lowPrice":"1714.72000000", + "volume":"298334.98780000", + "quoteVolume":"521926769.46721000", + "openTime":1687206042697, + "closeTime":1687292442697, + "firstId":1167040729, + "lastId":1167470154, + "count":429426 +} diff --git a/daemons/pricefeed/client/sources/bitfinex/bitfinex.go b/daemons/pricefeed/client/sources/bitfinex/bitfinex.go new file mode 100644 index 000000000..01f0712ca --- /dev/null +++ b/daemons/pricefeed/client/sources/bitfinex/bitfinex.go @@ -0,0 +1,122 @@ +package bitfinex + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// These indices into the REST API response are defined in https://docs.bitfinex.com/reference/rest-public-tickers +const ( + PairIndex = 0 + BidPriceIndex = 1 + AskPriceIndex = 3 + LastPriceIndex = 7 + // We don't need all 11 fields, but 11 fields indicates this is a valid API response. See above link + // for documentation on the response format. + BitfinexResponseLength = 11 +) + +// BitfinexTicker is our representation of the ticker information in Bitfinex API response. +// The raw response is a slice of floats. We use this constructed response to enable stricter +// validation. +// BitfinexTicker implements interface `Ticker` in util.go. +type BitfinexTicker struct { + Pair string `validate:"required"` + BidPrice string `validate:"required,positive-float-string"` + AskPrice string `validate:"required,positive-float-string"` + LastPrice string `validate:"required,positive-float-string"` +} + +// Ensure that BitfinexTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*BitfinexTicker)(nil) + +func (t BitfinexTicker) GetPair() string { + return t.Pair +} + +func (t BitfinexTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t BitfinexTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t BitfinexTicker) GetLastPrice() string { + return t.LastPrice +} + +// BitfinexPriceFunction transforms an API response from Bitfinex into a map of tickers +// to prices that have been shifted by a market specific exponent. +func BitfinexPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body into raw format first. + var rawResponse [][]interface{} + err = json.NewDecoder(response.Body).Decode(&rawResponse) + if err != nil { + return nil, nil, err + } + + // Convert raw tickers in response into a list of `BitfinexTicker`. + bitfinexTickers := []BitfinexTicker{} + invalidRawTickers := map[string]error{} + for _, rawTicker := range rawResponse { + // Verify raw ticker is the expected length. If not, continue to next. + if len(rawTicker) != BitfinexResponseLength { + continue + } + + // Get `pair` as `string`. If invalid, continue on to next raw ticker. + pair, ok := rawTicker[PairIndex].(string) + if !ok { + continue + } + // Get `bidPrice` as `float64`. If invalid, mark pair as invalid with bid price error. + bidPrice, ok := rawTicker[BidPriceIndex].(float64) + if !ok { + invalidRawTickers[pair] = errors.New("invalid bid price in response - not a float64") + continue + } + // Get `askPrice` as `float64`. If invalid, mark pair as invalid with ask price error. + askPrice, ok := rawTicker[AskPriceIndex].(float64) + if !ok { + invalidRawTickers[pair] = errors.New("invalid ask price in response - not a float64") + continue + } + // Get `lastPrice`. If invalid, mark pair as invalid with last price error. + lastPrice, ok := rawTicker[LastPriceIndex].(float64) + if !ok { + invalidRawTickers[pair] = errors.New("invalid last price in response - not a float64") + continue + } + bitfinexTickers = append(bitfinexTickers, BitfinexTicker{ + Pair: pair, + BidPrice: price_function.ConvertFloat64ToString(bidPrice), + AskPrice: price_function.ConvertFloat64ToString(askPrice), + LastPrice: price_function.ConvertFloat64ToString(lastPrice), + }) + } + + // Calculate median price of each ticker in `tickerToExponent`. + tickerToPrice, unavailableTickers, err = price_function.GetMedianPricesFromTickers( + bitfinexTickers, + tickerToExponent, + resolver, + ) + + // Mark as unavailable requested tickers whose raw ticker response was invalid. + for ticker, err := range invalidRawTickers { + if _, exists := tickerToExponent[ticker]; exists { + unavailableTickers[ticker] = err + } + } + + return tickerToPrice, unavailableTickers, err +} diff --git a/daemons/pricefeed/client/sources/bitfinex/exchange_query_details.go b/daemons/pricefeed/client/sources/bitfinex/exchange_query_details.go new file mode 100644 index 000000000..e05ead24c --- /dev/null +++ b/daemons/pricefeed/client/sources/bitfinex/exchange_query_details.go @@ -0,0 +1,15 @@ +package bitfinex + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + BitfinexDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_BITFINEX, + Url: "https://api-pub.bitfinex.com/v2/tickers?symbols=ALL", + PriceFunction: BitfinexPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/bitfinex/exchange_query_details_test.go b/daemons/pricefeed/client/sources/bitfinex/exchange_query_details_test.go new file mode 100644 index 000000000..70c5c53c5 --- /dev/null +++ b/daemons/pricefeed/client/sources/bitfinex/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package bitfinex_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitfinex" +) + +func TestBitfinexUrl(t *testing.T) { + require.Equal(t, "https://api-pub.bitfinex.com/v2/tickers?symbols=ALL", bitfinex.BitfinexDetails.Url) +} + +func TestBitfinexIsMultiMarket(t *testing.T) { + require.True(t, bitfinex.BitfinexDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/bitstamp/bitstamp.go b/daemons/pricefeed/client/sources/bitstamp/bitstamp.go new file mode 100644 index 000000000..3db57a635 --- /dev/null +++ b/daemons/pricefeed/client/sources/bitstamp/bitstamp.go @@ -0,0 +1,58 @@ +package bitstamp + +import ( + "encoding/json" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// BitstampTicker is our representation of ticker information returned in Bitstamp response. +// Need to implement interface `Ticker` in util.go. +type BitstampTicker struct { + Pair string `json:"pair" validate:"required"` + AskPrice string `json:"ask" validate:"required,positive-float-string"` + BidPrice string `json:"bid" validate:"required,positive-float-string"` + LastPrice string `json:"last" validate:"required,positive-float-string"` +} + +// Ensure that BitstampTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*BitstampTicker)(nil) + +func (t BitstampTicker) GetPair() string { + return t.Pair +} + +func (t BitstampTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t BitstampTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t BitstampTicker) GetLastPrice() string { + return t.LastPrice +} + +// BitstampPriceFunction transforms an API response from Bitstamp into a map of tickers to prices that have been +// shifted by a market specific exponent. +func BitstampPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body into a list of tickers. + var bitstampTickers []BitstampTicker + err = json.NewDecoder(response.Body).Decode(&bitstampTickers) + if err != nil { + return nil, nil, err + } + + return price_function.GetMedianPricesFromTickers( + bitstampTickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/bitstamp/bitstamp_test.go b/daemons/pricefeed/client/sources/bitstamp/bitstamp_test.go new file mode 100644 index 000000000..0b24b93b6 --- /dev/null +++ b/daemons/pricefeed/client/sources/bitstamp/bitstamp_test.go @@ -0,0 +1,209 @@ +package bitstamp_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitstamp" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Bitstamp. +const ( + BTCUSDC_TICKER = "BTC/USD" + ETHUSDC_TICKER = "ETH/USD" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +// Test response strings. +var ( + btcTicker = `{"timestamp": "1686600672", "open": "25940", "high": "26209", "low": "25634", "last": "25846", + "volume": "1903.95560640", "vwap": "25868", "bid": "25841", "ask": "25842", "open_24": "26183", + "percent_change_24": "-1.29", "pair": "BTC/USD"}` + ethTicker = `{"timestamp": "1686600672", "open": "1753.4", "high": "1777.7", "low": "1720.1", "last": "1734.9", + "volume": "6462.32622552", "vwap": "1738.6", "bid": "1734.3", "ask": "1734.9", "open_24": "1777.0", + "percent_change_24": "-2.37", "pair": "ETH/USD"}` + + BtcResponseString = fmt.Sprintf("[%s]", btcTicker) + EthResponseString = fmt.Sprintf("[%s]", ethTicker) + BtcAndEthResponseString = fmt.Sprintf("[%s,%s]", ethTicker, btcTicker) +) + +func TestBitstampPriceFunction_Mixed(t *testing.T) { + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: `[{"pair":"BTC/USD", "last":"26452", "ask":26453.23, "bid":"26449.38",}]`, + exponentMap: BtcExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to integer bidPrice when string was expected. + responseJsonString: `[{"pair":"BTC/USD", "last":"26452", "ask":26453.23, "bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "BitstampTicker.ask of type string"), + }, + "Unavailable - invalid type in response: malformed string": { + responseJsonString: `[{"pair":"BTC/USD", "last":"26452", "ask":"not a number", "bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("Key: 'BitstampTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: `[{}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC/USD"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: `[]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC/USD"), + }, + }, + "Unavailable - missing btc response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_734_900_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC/USD"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: `[{"pair":"BTC/USD", "last":"26452", "bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New( + "Key: 'BitstampTicker.AskPrice' Error:Field validation for 'AskPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Success - integers": { + responseJsonString: `[{"pair":"BTC/USD", "last":"26452", "ask":"26455", "bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_645_200_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_584_200_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: `[{"pair":"BTC/USD", "last":"26452.4415621293", "ask":"26455.51", "bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_645_244_156), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2584), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_584_200_000), + ETHUSDC_TICKER: uint64(1_734_900_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_734_900_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = bitstamp.BitstampPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = bitstamp.BitstampPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/bitstamp/exchange_query_details.go b/daemons/pricefeed/client/sources/bitstamp/exchange_query_details.go new file mode 100644 index 000000000..4d42242e6 --- /dev/null +++ b/daemons/pricefeed/client/sources/bitstamp/exchange_query_details.go @@ -0,0 +1,15 @@ +package bitstamp + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + BitstampDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_BITSTAMP, + Url: "https://www.bitstamp.net/api/v2/ticker/", + PriceFunction: BitstampPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/bitstamp/exchange_query_details_test.go b/daemons/pricefeed/client/sources/bitstamp/exchange_query_details_test.go new file mode 100644 index 000000000..be4297d25 --- /dev/null +++ b/daemons/pricefeed/client/sources/bitstamp/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package bitstamp_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitstamp" +) + +func TestBitstampUrl(t *testing.T) { + require.Equal(t, "https://www.bitstamp.net/api/v2/ticker/", bitstamp.BitstampDetails.Url) +} + +func TestBitstampIsMultiMarket(t *testing.T) { + require.True(t, bitstamp.BitstampDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/bybit/bybit.go b/daemons/pricefeed/client/sources/bybit/bybit.go new file mode 100644 index 000000000..932ac2331 --- /dev/null +++ b/daemons/pricefeed/client/sources/bybit/bybit.go @@ -0,0 +1,74 @@ +package bybit + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// BybitResponseBody defines the overall Bybit response. +type BybitResponseBody struct { + RetCode uint32 `json:"retCode" validate:"required"` + Result BybitResponseResult `json:"result" validate:"required"` +} + +// BybitResponseResult defines the `result` field of Bybit response. +type BybitResponseResult struct { + Tickers []BybitTicker `json:"list" validate:"required"` +} + +// BybitTicker is our representation of ticker information returned in Bybit response. +// It implements the Ticker interface in util.go. +type BybitTicker struct { + Pair string `json:"symbol" validate:"required"` + AskPrice string `json:"ask1Price" validate:"required,positive-float-string"` + BidPrice string `json:"bid1Price" validate:"required,positive-float-string"` + LastPrice string `json:"lastPrice" validate:"required,positive-float-string"` +} + +// Ensure that BybitTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*BybitTicker)(nil) + +func (t BybitTicker) GetPair() string { + return t.Pair +} + +func (t BybitTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t BybitTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t BybitTicker) GetLastPrice() string { + return t.LastPrice +} + +// BybitPriceFunction transforms an API response from Bybit into a map of tickers to prices that have been +// shifted by a market specific exponent. +func BybitPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body into a list of tickers. + var bybitResponseBody BybitResponseBody + err = json.NewDecoder(response.Body).Decode(&bybitResponseBody) + if err != nil { + return nil, nil, err + } + + if bybitResponseBody.RetCode != 0 { + return nil, nil, errors.New("response code is not 0") + } + + return price_function.GetMedianPricesFromTickers( + bybitResponseBody.Result.Tickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/bybit/bybit_test.go b/daemons/pricefeed/client/sources/bybit/bybit_test.go new file mode 100644 index 000000000..a2f07771c --- /dev/null +++ b/daemons/pricefeed/client/sources/bybit/bybit_test.go @@ -0,0 +1,217 @@ +package bybit_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bybit" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Bybit. +const ( + BTCUSDC_TICKER = "BTCUSDT" + ETHUSDC_TICKER = "ETHUSDT" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +// Test response strings. +var ( + btcTicker = `{"symbol":"BTCUSDT","bid1Price":"25920.44","bid1Size":"3.790133","ask1Price":"25920.45", + "ask1Size":"0.54281","lastPrice":"25922.39","prevPrice24h":"25899.68","price24hPcnt":"0.0009", + "highPrice24h":"26428.56","lowPrice24h":"25721.76","turnover24h":"151938440.056374", + "volume24h":"5833.570731","usdIndexPrice":"25918.96023518"}` + ethTicker = `{"symbol":"ETHUSDT","bid1Price":"1739.06","bid1Size":"39.30781","ask1Price":"1739.07", + "ask1Size":"30.25957","lastPrice":"1739.07","prevPrice24h":"1742.41","price24hPcnt":"-0.0019", + "highPrice24h":"1766.34","lowPrice24h":"1724.17","turnover24h":"92630762.1223188", + "volume24h":"53110.43746","usdIndexPrice":"1738.98142043"}` + ResponseStringTemplate = `{"retCode":0,"result":{"list":[%s]}}` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`{"retCode":0,"result":{"list":[%s,%s]}}`, btcTicker, ethTicker) +) + +func TestBybitPriceFunction_Mixed(t *testing.T) { + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","bid1Price":"1739.06","ask1Price":"1739.07","lastPrice":"1739.07",}`), + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to integer askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","bid1Price":"1739.06","ask1Price":1739.07,"lastPrice":"1739.07"}`), + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "BybitTicker.result.list.ask1Price of type string"), + }, + "Unavailable - invalid type in response: malformed string": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","bid1Price":"not a number","ask1Price":"1739.07","lastPrice":"1739.07"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'BybitTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTCUSDT"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTCUSDT"), + }, + }, + "Unavailable - missing btc response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_739_070_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTCUSDT"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","bid1Price":"1739.06","ask1Price":"1739.07"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'BybitTicker.LastPrice' Error:Field validation for 'LastPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETHUSDT","bid1Price":"1739","ask1Price":"1739","lastPrice":"1739.07"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_739_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_592_045_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"BTCUSDT","bid1Price":"25920.44","ask1Price":"25921.9423714","lastPrice":"25922.52"}`), + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_592_194_237), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_592), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_592_045_000), + ETHUSDC_TICKER: uint64(1_739_070_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_739_070_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = bybit.BybitPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = bybit.BybitPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/bybit/exchange_query_details.go b/daemons/pricefeed/client/sources/bybit/exchange_query_details.go new file mode 100644 index 000000000..4df2ecd30 --- /dev/null +++ b/daemons/pricefeed/client/sources/bybit/exchange_query_details.go @@ -0,0 +1,15 @@ +package bybit + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + BybitDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_BYBIT, + Url: "https://api.bybit.com/v5/market/tickers?category=spot", + PriceFunction: BybitPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/bybit/exchange_query_details_test.go b/daemons/pricefeed/client/sources/bybit/exchange_query_details_test.go new file mode 100644 index 000000000..3053022d3 --- /dev/null +++ b/daemons/pricefeed/client/sources/bybit/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package bybit_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bybit" +) + +func TestBybitUrl(t *testing.T) { + require.Equal(t, "https://api.bybit.com/v5/market/tickers?category=spot", bybit.BybitDetails.Url) +} + +func TestBybitIsMultiMarket(t *testing.T) { + require.True(t, bybit.BybitDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro.go b/daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro.go new file mode 100644 index 000000000..54e93e896 --- /dev/null +++ b/daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro.go @@ -0,0 +1,72 @@ +package coinbase_pro + +import ( + "encoding/json" + "net/http" + + "github.com/tellor-io/layer/daemons/exchange_common" + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// CoinbaseProTicker is our representation of ticker information returned in CoinbasePro response. +// CoinbaseProTicker implements interface `Ticker` in util.go. +type CoinbaseProTicker struct { + // `Pair` is not part of API response but can be set manually to reuse existing helper functions. + Pair string `validate:"required"` + AskPrice string `json:"ask" validate:"required,positive-float-string"` + BidPrice string `json:"bid" validate:"required,positive-float-string"` + LastPrice string `json:"price" validate:"required,positive-float-string"` +} + +// Ensure that CoinbaseProTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*CoinbaseProTicker)(nil) + +func (t CoinbaseProTicker) GetPair() string { + return t.Pair +} + +func (t CoinbaseProTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t CoinbaseProTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t CoinbaseProTicker) GetLastPrice() string { + return t.LastPrice +} + +// CoinbaseProPriceFunction transforms an API response from CoinbasePro into a map of tickers +// to prices that have been shifted by a market specific exponent. +func CoinbaseProPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Get ticker. The API response should only contain information for one market. + ticker, _, err := price_function.GetOnlyTickerAndExponent( + tickerToExponent, + exchange_common.EXCHANGE_ID_COINBASE_PRO, + ) + if err != nil { + return nil, nil, err + } + + // Unmarshal response body. + var coinbaseProTicker CoinbaseProTicker + err = json.NewDecoder(response.Body).Decode(&coinbaseProTicker) + if err != nil { + return nil, nil, err + } + + // Invoke `GetMedianPricesFromTickers` on a list of one ticker whose `Pair` + // matches the only ticker in `tickerToExponent`. + coinbaseProTicker.Pair = ticker + return price_function.GetMedianPricesFromTickers( + []CoinbaseProTicker{coinbaseProTicker}, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro_test.go b/daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro_test.go new file mode 100644 index 000000000..558458be4 --- /dev/null +++ b/daemons/pricefeed/client/sources/coinbase_pro/coinbase_pro_test.go @@ -0,0 +1,187 @@ +package coinbase_pro_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/coinbase_pro" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Coinbase Pro. +const ( + BTCUSDC_TICKER = "BTC-USD" + ETHUSDC_TICKER = "ETH-USD" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestCoinbaseProPriceFunction_Mixed(t *testing.T) { + // Test response strings. + var ( + BtcResponseString = pricefeed.ReadJsonTestFile(t, "btc_ticker.json") + ) + + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: `{"ask":"1662.49","bid":"1662.41","price":"1662.48",}`, + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to number lastPrice when string was expected. + responseJsonString: `{"ask":"1662.49","bid":"1662.41","price":1662.48}`, + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "CoinbaseProTicker.price of type string"), + }, + "Unavailable - bid price is 0": { + responseJsonString: `{"ask":"1662.49","bid":"0","price":"1662.48"}`, + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'CoinbaseProTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - ask price is negative": { + responseJsonString: `{"ask":"-1662.49","bid":"1662.41","price":"1662.48"}`, + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'CoinbaseProTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - last price is negative": { + responseJsonString: `{"ask":"1662.49","bid":"1662.41","price":"-1662.48"}`, + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'CoinbaseProTicker.LastPrice' Error:Field validation for " + + "'LastPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: `{}`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("Key: 'CoinbaseProTicker.AskPrice' Error:Field validation for 'AskPrice' " + + "failed on the 'required' tag\nKey: 'CoinbaseProTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'required' tag\nKey: 'CoinbaseProTicker.LastPrice' Error:Field validation " + + "for 'LastPrice' failed on the 'required' tag"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: `{"bid":"1662.41","price":"1662.48"}`, + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'CoinbaseProTicker.AskPrice' Error:Field validation for 'AskPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Success - integers": { + responseJsonString: `{"ask":"1662","bid":"1662","price":"1662.48"}`, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_662_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_549_981_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: `{"ask":"1662","bid":"1662.23124383529642","price":"1662.48"}`, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_662_231_243), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_549), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = coinbase_pro.CoinbaseProPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = coinbase_pro.CoinbaseProPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details.go b/daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details.go new file mode 100644 index 000000000..2a91a45d1 --- /dev/null +++ b/daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details.go @@ -0,0 +1,14 @@ +package coinbase_pro + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + CoinbaseProDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_COINBASE_PRO, + Url: "https://api.pro.coinbase.com/products/$/ticker", + PriceFunction: CoinbaseProPriceFunction, + } +) diff --git a/daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details_test.go b/daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details_test.go new file mode 100644 index 000000000..84da42a45 --- /dev/null +++ b/daemons/pricefeed/client/sources/coinbase_pro/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package coinbase_pro_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/coinbase_pro" +) + +func TestCoinbaseProUrl(t *testing.T) { + require.Equal(t, "https://api.pro.coinbase.com/products/$/ticker", coinbase_pro.CoinbaseProDetails.Url) +} + +func TestCoinbaseProIsMultiMarket(t *testing.T) { + require.False(t, coinbase_pro.CoinbaseProDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/coinbase_pro/testdata/btc_ticker.json b/daemons/pricefeed/client/sources/coinbase_pro/testdata/btc_ticker.json new file mode 100644 index 000000000..9c1ec98c2 --- /dev/null +++ b/daemons/pricefeed/client/sources/coinbase_pro/testdata/btc_ticker.json @@ -0,0 +1,9 @@ +{ + "ask":"25499.81", + "bid":"25499.8", + "volume":"17697.96099093", + "trade_id":539894971, + "price":"25499.81", + "size":"0.00003172", + "time":"2023-06-16T02:26:52.037760Z" +} diff --git a/daemons/pricefeed/client/sources/crypto_com/crypto_com.go b/daemons/pricefeed/client/sources/crypto_com/crypto_com.go new file mode 100644 index 000000000..4696c3c0e --- /dev/null +++ b/daemons/pricefeed/client/sources/crypto_com/crypto_com.go @@ -0,0 +1,76 @@ +package crypto_com + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// CryptoComResponseBody defines the overall CryptoCom response. +type CryptoComResponseBody struct { + Code uint32 `json:"code" validate:"required"` + Result CryptoComResponseResult `json:"result" validate:"required"` +} + +// CryptoComResponseResult defines the `result` field of CryptoCom response. +type CryptoComResponseResult struct { + Tickers []CryptoComTicker `json:"data" validate:"required"` +} + +// CryptoComTicker is our representation of ticker information returned in CryptoCom response. +// Need to implement interface `Ticker` in util.go. +// Note: CryptoCom returns `null` for bids and asks if there are none on the orderbook, in which +// case we mark the ticker as unavailable. +type CryptoComTicker struct { + Pair string `json:"i" validate:"required"` + AskPrice string `json:"k" validate:"required,positive-float-string"` + BidPrice string `json:"b" validate:"required,positive-float-string"` + LastPrice string `json:"a" validate:"required,positive-float-string"` +} + +// Ensure that CryptoComTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*CryptoComTicker)(nil) + +func (t CryptoComTicker) GetPair() string { + return t.Pair +} + +func (t CryptoComTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t CryptoComTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t CryptoComTicker) GetLastPrice() string { + return t.LastPrice +} + +// CryptoComPriceFunction transforms an API response from CryptoCom into a map of tickers to prices that have been +// shifted by a market specific exponent. +func CryptoComPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body into a list of tickers. + var cryptoComResponseBody CryptoComResponseBody + err = json.NewDecoder(response.Body).Decode(&cryptoComResponseBody) + if err != nil { + return nil, nil, err + } + + if cryptoComResponseBody.Code != 0 { + return nil, nil, errors.New("response code is not 0") + } + + return price_function.GetMedianPricesFromTickers( + cryptoComResponseBody.Result.Tickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/crypto_com/crypto_com_test.go b/daemons/pricefeed/client/sources/crypto_com/crypto_com_test.go new file mode 100644 index 000000000..f0e14ee60 --- /dev/null +++ b/daemons/pricefeed/client/sources/crypto_com/crypto_com_test.go @@ -0,0 +1,238 @@ +package crypto_com_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/crypto_com" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for CryptoCom. +const ( + BTCUSDC_TICKER = "BTC_USD" + ETHUSDC_TICKER = "ETH_USD" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +// Test response strings. +var ( + btcTicker = `{"i":"BTC_USD","h":"26437.90","l":"25722.02","a":"25988.88","v":"617.7597", + "vv":"16062822.01","c":"-0.0011","b":"25989.77","k":"25990.15","t":1686716261608}` + ethTicker = `{"i":"ETH_USD","h":"1767.34","l":"1724.35","a":"1745.33","v":"4854.0452", + "vv":"8458590.29","c":"-0.0005","b":"1745.21","k":"1745.33","t":1686716257576}` + ResponseStringTemplate = `{"code":0,"result":{"data":[%s]}}` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`{"code":0,"result":{"data":[%s,%s]}}`, btcTicker, ethTicker) +) + +func TestCryptoComPriceFunction_Mixed(t *testing.T) { + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988.88","b":"25989.77","k":"25990.15",}`), + exponentMap: BtcExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to integer askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988.88","b":"25989.77","k":25990.15}`), + exponentMap: BtcExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "CryptoComTicker.result.data.k of type string"), + }, + "Unavailable - invalid type in response: malformed string": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988.88","b":"malformed number","k":"25990.15"}, + {"i":"ETH_USD","a":"1745.33","b":"1745.21","k":"1745.33"}`), + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_745_330_000), // ETH_USD should still be available despite BTC_USD error. + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("Key: 'CryptoComTicker.BidPrice' Error:Field validation for 'BidPrice' " + + "failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USD"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USD"), + }, + }, + "Unavailable - missing btc response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_745_330_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USD"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988.88","k":"25990.15"}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New( + "Key: 'CryptoComTicker.BidPrice' Error:Field validation for 'BidPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988","b":"25989","k":"25990.15"}`), + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_598_900_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_598_977_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988.88","b":"25989.775294213","k":"25990.15"}`), + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_598_977_529), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_598), + }, + }, + "Success - null bid price": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25990.88","b":null,"k":"25988.15"}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New( + "Key: 'CryptoComTicker.BidPrice' Error:Field validation for 'BidPrice' failed on the 'required' tag", + ), + }, + }, + "Success - null ask prices": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"i":"BTC_USD","a":"25988.88","b":"25984.23","k":null}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New( + "Key: 'CryptoComTicker.AskPrice' Error:Field validation for 'AskPrice' failed on the 'required' tag", + ), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_598_977_000), + ETHUSDC_TICKER: uint64(1_745_330_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_745_330_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = crypto_com.CryptoComPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = crypto_com.CryptoComPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/crypto_com/exchange_query_details.go b/daemons/pricefeed/client/sources/crypto_com/exchange_query_details.go new file mode 100644 index 000000000..f1436caa8 --- /dev/null +++ b/daemons/pricefeed/client/sources/crypto_com/exchange_query_details.go @@ -0,0 +1,15 @@ +package crypto_com + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + CryptoComDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_CRYPTO_COM, + Url: "https://api.crypto.com/v2/public/get-ticker", + PriceFunction: CryptoComPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/crypto_com/exchange_query_details_test.go b/daemons/pricefeed/client/sources/crypto_com/exchange_query_details_test.go new file mode 100644 index 000000000..ec83f815b --- /dev/null +++ b/daemons/pricefeed/client/sources/crypto_com/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package crypto_com_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/crypto_com" +) + +func TestCryptoComUrl(t *testing.T) { + require.Equal(t, "https://api.crypto.com/v2/public/get-ticker", crypto_com.CryptoComDetails.Url) +} + +func TestCryptoComIsMultiMarket(t *testing.T) { + require.True(t, crypto_com.CryptoComDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/exchange_error.go b/daemons/pricefeed/client/sources/exchange_error.go new file mode 100644 index 000000000..ac8ddcc80 --- /dev/null +++ b/daemons/pricefeed/client/sources/exchange_error.go @@ -0,0 +1,41 @@ +package sources + +import ( + "fmt" + + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// ExchangeError describes an error that is specific to a particular exchange. These errors are emitted +// by an exchange's price function whenever the exchange's API returns a valid http response. +type ExchangeError interface { + error + GetExchangeId() types.ExchangeId +} + +// Ensure ExcahngeErrorImpl implements ExchangeError at compile time. +var _ ExchangeError = &ExchangeErrorImpl{} + +// ExchangeErrorImpl implements ExchangeError. +type ExchangeErrorImpl struct { + exchangeId types.ExchangeId + err error +} + +// Error returns a string representation of the error. +func (e *ExchangeErrorImpl) Error() string { + return fmt.Sprintf("%v exchange error: %v", e.exchangeId, e.err) +} + +// GetExchangeId returns the exchange id associated with the error. +func (e *ExchangeErrorImpl) GetExchangeId() types.ExchangeId { + return e.exchangeId +} + +// NewExchangeError returns a new ExchangeError. +func NewExchangeError(exchangeId types.ExchangeId, msg string) ExchangeError { + return &ExchangeErrorImpl{ + exchangeId: exchangeId, + err: fmt.Errorf(msg), + } +} diff --git a/daemons/pricefeed/client/sources/exchange_error_test.go b/daemons/pricefeed/client/sources/exchange_error_test.go new file mode 100644 index 000000000..146e5a6b5 --- /dev/null +++ b/daemons/pricefeed/client/sources/exchange_error_test.go @@ -0,0 +1,18 @@ +package sources_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources" +) + +func TestExchangeError(t *testing.T) { + error := sources.NewExchangeError("exchange", "error") + var exchangeError sources.ExchangeError + found := errors.As(error, &exchangeError) + require.True(t, found) + require.Equal(t, error, exchangeError) + require.Equal(t, "exchange", exchangeError.GetExchangeId()) +} diff --git a/daemons/pricefeed/client/sources/gate/exchange_query_details.go b/daemons/pricefeed/client/sources/gate/exchange_query_details.go new file mode 100644 index 000000000..fc77f67c8 --- /dev/null +++ b/daemons/pricefeed/client/sources/gate/exchange_query_details.go @@ -0,0 +1,15 @@ +package gate + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + GateDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_GATE, + Url: "https://api.gateio.ws/api/v4/spot/tickers", + PriceFunction: GatePriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/gate/exchange_query_details_test.go b/daemons/pricefeed/client/sources/gate/exchange_query_details_test.go new file mode 100644 index 000000000..53f95a4cb --- /dev/null +++ b/daemons/pricefeed/client/sources/gate/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package gate_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/gate" +) + +func TestGateUrl(t *testing.T) { + require.Equal(t, "https://api.gateio.ws/api/v4/spot/tickers", gate.GateDetails.Url) +} + +func TestGateIsMultiMarket(t *testing.T) { + require.True(t, gate.GateDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/gate/gate.go b/daemons/pricefeed/client/sources/gate/gate.go new file mode 100644 index 000000000..107ffbc38 --- /dev/null +++ b/daemons/pricefeed/client/sources/gate/gate.go @@ -0,0 +1,58 @@ +package gate + +import ( + "encoding/json" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// GateTicker is our representation of ticker information returned in Gate response. +// Need to implement interface `Ticker` in util.go. +type GateTicker struct { + Pair string `json:"currency_pair" validate:"required"` + AskPrice string `json:"lowest_ask" validate:"required,positive-float-string"` + BidPrice string `json:"highest_bid" validate:"required,positive-float-string"` + LastPrice string `json:"last" validate:"required,positive-float-string"` +} + +// Ensure that GateTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*GateTicker)(nil) + +func (t GateTicker) GetPair() string { + return t.Pair +} + +func (t GateTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t GateTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t GateTicker) GetLastPrice() string { + return t.LastPrice +} + +// GatePriceFunction transforms an API response from Gate into a map of tickers to prices that have been +// shifted by a market specific exponent. +func GatePriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body into a list of tickers. + var gateTickers []GateTicker + err = json.NewDecoder(response.Body).Decode(&gateTickers) + if err != nil { + return nil, nil, err + } + + return price_function.GetMedianPricesFromTickers( + gateTickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/gate/gate_test.go b/daemons/pricefeed/client/sources/gate/gate_test.go new file mode 100644 index 000000000..6373c998c --- /dev/null +++ b/daemons/pricefeed/client/sources/gate/gate_test.go @@ -0,0 +1,214 @@ +package gate_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/gate" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Gate. +const ( + BTCUSDC_TICKER = "BTC_USD" + ETHUSDC_TICKER = "ETH_USD" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +// Test response strings. +var ( + btcTicker = `{"currency_pair":"BTC_USD","last":"26452.44","lowest_ask":"26455.51","highest_bid":"26449.38", + "change_percentage":"0.25","base_volume":"162.03142","quote_volume":"4294834.9535027","high_24h":"26825.72", + "low_24h":"26308.05"}` + ethTicker = `{"currency_pair":"ETH_USD","last":"1757.36","lowest_ask":"1757.96","highest_bid":"1757.71", + "change_percentage":"-3.94","change_utc0":"0","change_utc8":"0.88","base_volume":"1249.407483", + "quote_volume":"2197610.5129584","high_24h":"1829.58","low_24h":"1722.33"}` + + BtcResponseString = fmt.Sprintf("[%s]", btcTicker) + EthResponseString = fmt.Sprintf("[%s]", ethTicker) + BtcAndEthResponseString = fmt.Sprintf("[%s,%s]", ethTicker, btcTicker) +) + +func TestGatePriceFunction_Mixed(t *testing.T) { + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: `[{"currency_pair":"BTC_USD","last":"26452", + "lowest_ask":26453.23,"highest_bid":"26449.38",}]`, + exponentMap: BtcExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to integer bidPrice when string was expected. + responseJsonString: `[{"currency_pair":"BTC_USD","last":"26452", + "lowest_ask":26453.23,"highest_bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "GateTicker.lowest_ask of type string"), + }, + "Unavailable - invalid type in response: malformed string": { + responseJsonString: `[{"currency_pair":"BTC_USD","last":"26452", + "lowest_ask":"not a number","highest_bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("Key: 'GateTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: `[{}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USD"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: `[]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USD"), + }, + }, + "Unavailable - missing btc response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_757_710_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USD"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: `[{"currency_pair":"BTC_USD","last":"26452","highest_bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New( + "Key: 'GateTicker.AskPrice' Error:Field validation for 'AskPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Success - integers": { + responseJsonString: `[{"currency_pair":"BTC_USD","last":"26452", + "lowest_ask":"26455","highest_bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_645_200_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_645_244_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: `[{"currency_pair":"BTC_USD","last":"26452.4415621293", + "lowest_ask":"26455.51","highest_bid":"26449.38"}]`, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_645_244_156), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2645), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_645_244_000), + ETHUSDC_TICKER: uint64(1_757_710_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_757_710_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = gate.GatePriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = gate.GatePriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/huobi/exchange_query_details.go b/daemons/pricefeed/client/sources/huobi/exchange_query_details.go new file mode 100644 index 000000000..7901748e7 --- /dev/null +++ b/daemons/pricefeed/client/sources/huobi/exchange_query_details.go @@ -0,0 +1,15 @@ +package huobi + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + HuobiDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_HUOBI, + Url: "https://api.huobi.pro/market/tickers", + PriceFunction: HuobiPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/huobi/exchange_query_details_test.go b/daemons/pricefeed/client/sources/huobi/exchange_query_details_test.go new file mode 100644 index 000000000..8fa73fb0f --- /dev/null +++ b/daemons/pricefeed/client/sources/huobi/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package huobi_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/huobi" +) + +func TestHuobiUrl(t *testing.T) { + require.Equal(t, "https://api.huobi.pro/market/tickers", huobi.HuobiDetails.Url) +} + +func TestHuobiIsMultiMarket(t *testing.T) { + require.True(t, huobi.HuobiDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/huobi/huobi.go b/daemons/pricefeed/client/sources/huobi/huobi.go new file mode 100644 index 000000000..442e90253 --- /dev/null +++ b/daemons/pricefeed/client/sources/huobi/huobi.go @@ -0,0 +1,69 @@ +package huobi + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// HuobiResponseBody defines the overall Huobi response. +type HuobiResponseBody struct { + Status string `json:"status" validate:"required"` + Tickers []HuobiTicker `json:"data" validate:"required"` +} + +// HuobiTicker is our representation of ticker information returned in Huobi response. +// HuobiTicker implements interface `Ticker` in util.go. +type HuobiTicker struct { + Pair string `json:"symbol" validate:"required"` + AskPrice float64 `json:"ask" validate:"required,gt=0"` + BidPrice float64 `json:"bid" validate:"required,gt=0"` + LastPrice float64 `json:"close" validate:"required,gt=0"` +} + +// Ensure that HuobiTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*HuobiTicker)(nil) + +func (t HuobiTicker) GetPair() string { + return t.Pair +} + +func (t HuobiTicker) GetAskPrice() string { + return price_function.ConvertFloat64ToString(t.AskPrice) +} + +func (t HuobiTicker) GetBidPrice() string { + return price_function.ConvertFloat64ToString(t.BidPrice) +} + +func (t HuobiTicker) GetLastPrice() string { + return price_function.ConvertFloat64ToString(t.LastPrice) +} + +// HuobiPriceFunction transforms an API response from Huobi into a map of tickers to prices that have been +// shifted by a market specific exponent. +func HuobiPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body. + var huobiResponseBody HuobiResponseBody + err = json.NewDecoder(response.Body).Decode(&huobiResponseBody) + if err != nil { + return nil, nil, err + } + + if huobiResponseBody.Status != "ok" { + return nil, nil, errors.New(`huobi response status is not "ok"`) + } + + return price_function.GetMedianPricesFromTickers( + huobiResponseBody.Tickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/huobi/huobi_test.go b/daemons/pricefeed/client/sources/huobi/huobi_test.go new file mode 100644 index 000000000..9ae3ff66e --- /dev/null +++ b/daemons/pricefeed/client/sources/huobi/huobi_test.go @@ -0,0 +1,237 @@ +package huobi_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/huobi" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Huobi. +const ( + BTCUSDC_TICKER = "btcusdt" + ETHUSDC_TICKER = "ethusdt" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestHuobiPriceFunction_Mixed(t *testing.T) { + // Test response strings. + var ( + btcTicker = pricefeed.ReadJsonTestFile(t, "btc_ticker.json") + ethTicker = pricefeed.ReadJsonTestFile(t, "eth_ticker.json") + + ResponseStringTemplate = `{"status":"ok","data":[%s]}` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`{"status":"ok","data":[%s,%s]}`, btcTicker, ethTicker) + ) + + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","close":1648.74,"bid":1648.95,"ask":1648.96,}`), + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: string": { + // Invalid due to integer askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","close":1648.74,"bid":"1648.95","ask":1648.96}`), + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal string into Go struct field " + + "HuobiTicker.data.bid of type float64"), + }, + "Unavailable - bid price is 0": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","close":1648.74,"bid":0,"ask":1648.96}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'HuobiTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'required' tag"), + }, + }, + "Unavailable - ask price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","close":1648.74,"bid":1648.96,"ask":-0.2}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'HuobiTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'gt' tag"), + }, + }, + "Unavailable - last price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","close":-0.5,"bid":1648.96,"ask":1648.24}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'HuobiTicker.LastPrice' Error:Field validation for " + + "'LastPrice' failed on the 'gt' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker btcusdt"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker btcusdt"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","bid":1648.95,"ask":1648.96}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'HuobiTicker.LastPrice' Error:Field validation for 'LastPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - response status is not ok": { + responseJsonString: `{"status":"err","data":[]}`, + exponentMap: EthExponentMap, + expectedError: errors.New(`huobi response status is not "ok"`), + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Mixed - missing btc response and has eth response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_648_950_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker btcusdt"), + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ethusdt","close":1648,"bid":1649,"ask":1648}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_648_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_507_701_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"btcusdt","close":25077.99,"bid":25077.0,"ask":25077.0175230123572}`), + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_507_701_752), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_507), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_507_701_000), + ETHUSDC_TICKER: uint64(1_648_950_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_648_950_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = huobi.HuobiPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = huobi.HuobiPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/huobi/testdata/btc_ticker.json b/daemons/pricefeed/client/sources/huobi/testdata/btc_ticker.json new file mode 100644 index 000000000..57049622d --- /dev/null +++ b/daemons/pricefeed/client/sources/huobi/testdata/btc_ticker.json @@ -0,0 +1,14 @@ +{ + "symbol":"btcusdt", + "open":25984.99, + "high":26096.0, + "low":24800.7, + "close":25077.99, + "amount":4375.245351673388, + "vol":1.1212023704534914E8, + "count":98168, + "bid":25077.0, + "bidSize":1.097061, + "ask":25077.01, + "askSize":0.15 +} diff --git a/daemons/pricefeed/client/sources/huobi/testdata/eth_ticker.json b/daemons/pricefeed/client/sources/huobi/testdata/eth_ticker.json new file mode 100644 index 000000000..710874e9c --- /dev/null +++ b/daemons/pricefeed/client/sources/huobi/testdata/eth_ticker.json @@ -0,0 +1,14 @@ +{ + "symbol":"ethusdt", + "open":1740.89, + "high":1747.83, + "low":1626.25, + "close":1648.74, + "amount":26994.982328671817, + "vol":4.585927637768373E7, + "count":45292, + "bid":1648.95, + "bidSize":3.88, + "ask":1648.96, + "askSize":1.49 +} diff --git a/daemons/pricefeed/client/sources/kraken/exchange_query_details.go b/daemons/pricefeed/client/sources/kraken/exchange_query_details.go new file mode 100644 index 000000000..6f32a081e --- /dev/null +++ b/daemons/pricefeed/client/sources/kraken/exchange_query_details.go @@ -0,0 +1,15 @@ +package kraken + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + KrakenDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_KRAKEN, + Url: "https://api.kraken.com/0/public/Ticker", + PriceFunction: KrakenPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/kraken/exchange_query_details_test.go b/daemons/pricefeed/client/sources/kraken/exchange_query_details_test.go new file mode 100644 index 000000000..ca949a8b7 --- /dev/null +++ b/daemons/pricefeed/client/sources/kraken/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package kraken_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kraken" +) + +func TestKrakenUrl(t *testing.T) { + require.Equal(t, "https://api.kraken.com/0/public/Ticker", kraken.KrakenDetails.Url) +} + +func TestKrakenIsMultiMarket(t *testing.T) { + require.True(t, kraken.KrakenDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/kraken/kraken.go b/daemons/pricefeed/client/sources/kraken/kraken.go new file mode 100644 index 000000000..6dd4e0d03 --- /dev/null +++ b/daemons/pricefeed/client/sources/kraken/kraken.go @@ -0,0 +1,92 @@ +package kraken + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// https://api.kraken.com/0/public/Ticker +// https://docs.kraken.com/rest/#tag/Market-Data/operation/getTickerInformation +// KrakenTickerResult is the result of a Kraken API call for a single ticker. +// KrakentickerResult implements the types.TickerResult interface in util.go. +type KrakenTickerResult struct { + pair string + AskPriceStats []string `json:"a" validate:"len=3,dive,positive-float-string"` + BidPriceStats []string `json:"b" validate:"len=3,dive,positive-float-string"` + ClosePriceStats []string `json:"c" validate:"len=2,dive,positive-float-string"` +} + +// Ensure that KrakenTickerResult implements the TickerResult interface at compile time. +var _ price_function.Ticker = (*KrakenTickerResult)(nil) + +func (ktr KrakenTickerResult) WithPair(pair string) KrakenTickerResult { + ktr.pair = pair + return ktr +} + +func (ktr KrakenTickerResult) GetPair() string { + return ktr.pair +} + +func (ktr KrakenTickerResult) GetAskPrice() string { + return ktr.AskPriceStats[0] +} + +func (ktr KrakenTickerResult) GetBidPrice() string { + return ktr.BidPriceStats[0] +} + +func (ktr KrakenTickerResult) GetLastPrice() string { + return ktr.ClosePriceStats[0] +} + +type KrakenResponseBody struct { + // As of this time, the Kraken API response is all-or-nothing - either valid ticker data, or one or more errors, + // but not both. We enforce this expectation by defining mutual exclusivity in the validation tags of the Errors + // field so that any validated API result always meets our expectation in the response parsing logic. + Errors []string `json:"error" validate:"omitempty"` + Tickers map[string]KrakenTickerResult `validate:"required_without=Errors,excluded_with=Errors,dive" json:"result"` +} + +// KrakenPriceFunction transforms an API response from Kraken into a map of tickers to prices that have been +// shifted by a market-specific exponent. +func KrakenPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + var responseBody KrakenResponseBody + if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { + return nil, nil, err + } + // The Kraken API will return an empty list of errors with an API result containing valid tickers. However, it's + // easier for us to validate that there were no errors if this field is set to nil whenever it's empty. + if len(responseBody.Errors) == 0 { + responseBody.Errors = nil + } + + if len(responseBody.Errors) > 0 { + // TODO(CORE-185): Update to Go 1.20 and replace strings.Join with errors.Join. + apiCallError := fmt.Errorf( + "kraken API call error: %w", errors.New(strings.Join(responseBody.Errors, ", ")), + ) + return nil, nil, apiCallError + } + + tickers := make([]KrakenTickerResult, 0, len(responseBody.Tickers)) + for pair, ticker := range responseBody.Tickers { + tickers = append(tickers, ticker.WithPair(pair)) + } + + return price_function.GetMedianPricesFromTickers( + tickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/kraken/kraken_test.go b/daemons/pricefeed/client/sources/kraken/kraken_test.go new file mode 100644 index 000000000..4a96a65fa --- /dev/null +++ b/daemons/pricefeed/client/sources/kraken/kraken_test.go @@ -0,0 +1,124 @@ +package kraken_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kraken" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +const ( + ETHUSDC_TICKER = "XETHZUSD" + BTCUSDC_TICKER = "XXBTZUSD" +) + +var ( + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestKrakenPriceFunction_Mixed(t *testing.T) { + krakenValidResponseString := pricefeed.ReadJsonTestFile(t, "kraken_2_ticker_response.json") + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Failure - invalid response, not JSON": { + // Invalid due to trailing comma in JSON. + responseJsonString: `{,}`, + exponentMap: EthExponentMap, + expectedError: errors.New( + "invalid character ',' looking for beginning of object key string", + ), + }, + "Failure - invalid response, float instead of string data type, missing": { + // Invalid due to trailing comma in JSON. + responseJsonString: `{"result":{"XETHZUSD":{"a":[2105.8]}}}`, + exponentMap: BtcAndEthExponentMap, + expectedError: errors.New( + "json: cannot unmarshal number into Go struct field KrakenTickerResult.result.a of type string", + ), + }, + "Unavailable - overflow due to negative exponent": { + // Invalid due to trailing comma in JSON. + responseJsonString: krakenValidResponseString, + exponentMap: map[string]int32{ETHUSDC_TICKER: -3000}, + expectedPriceMap: map[string]uint64{}, + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Unavailable - fails on medianization error": { + responseJsonString: krakenValidResponseString, + exponentMap: BtcAndEthExponentMap, + medianFunctionFails: true, + expectedPriceMap: map[string]uint64{}, + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: testutil.MedianizationError, + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Failure - Kraken API Error response": { + responseJsonString: `{"error":["EQuery:Unknown asset pair"]}`, + exponentMap: EthExponentMap, + expectedError: errors.New("kraken API call error: EQuery:Unknown asset pair"), + }, + "Success - one market response": { + responseJsonString: krakenValidResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_888_000_000), + }, + }, + "Success - two market response": { + responseJsonString: krakenValidResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_888_000_000), + BTCUSDC_TICKER: uint64(2_920_750_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = kraken.KrakenPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = kraken.KrakenPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/kraken/testdata/kraken_2_ticker_response.json b/daemons/pricefeed/client/sources/kraken/testdata/kraken_2_ticker_response.json new file mode 100644 index 000000000..02beeb2c2 --- /dev/null +++ b/daemons/pricefeed/client/sources/kraken/testdata/kraken_2_ticker_response.json @@ -0,0 +1,79 @@ +{ + "error": [], + "result": { + "XETHZUSD": { + "a": [ + "1888.00000", + "59", + "59.000" + ], + "b": [ + "1887.99000", + "62", + "62.000" + ], + "c": [ + "1888.00000", + "0.65587578" + ], + "v": [ + "17367.55023748", + "31422.36250590" + ], + "p": [ + "1903.84415", + "1910.75540" + ], + "t": [ + 7643, + 12602 + ], + "l": [ + "1875.11000", + "1875.11000" + ], + "h": [ + "1923.59000", + "1940.15000" + ], + "o": "1908.73000" + }, + "XXBTZUSD": { + "a": [ + "29207.50000", + "1", + "1.000" + ], + "b": [ + "29204.50000", + "2", + "2.000" + ], + "c": [ + "29207.50000", + "0.01327170" + ], + "v": [ + "2780.31893666", + "5239.61837641" + ], + "p": [ + "29267.80173", + "29411.47287" + ], + "t": [ + 27258, + 43204 + ], + "l": [ + "28904.60000", + "28904.60000" + ], + "h": [ + "29605.00000", + "29882.10000" + ], + "o": "29476.80000" + } + } +} diff --git a/daemons/pricefeed/client/sources/kucoin/exchange_query_details.go b/daemons/pricefeed/client/sources/kucoin/exchange_query_details.go new file mode 100644 index 000000000..797fb7aa6 --- /dev/null +++ b/daemons/pricefeed/client/sources/kucoin/exchange_query_details.go @@ -0,0 +1,15 @@ +package kucoin + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + KucoinDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_KUCOIN, + Url: "https://api.kucoin.com/api/v1/market/allTickers", + PriceFunction: KucoinPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/kucoin/exchange_query_details_test.go b/daemons/pricefeed/client/sources/kucoin/exchange_query_details_test.go new file mode 100644 index 000000000..8d8967b05 --- /dev/null +++ b/daemons/pricefeed/client/sources/kucoin/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package kucoin_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kucoin" +) + +func TestKucoinUrl(t *testing.T) { + require.Equal(t, "https://api.kucoin.com/api/v1/market/allTickers", kucoin.KucoinDetails.Url) +} + +func TestKucoinIsMultiMarket(t *testing.T) { + require.True(t, kucoin.KucoinDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/kucoin/kucoin.go b/daemons/pricefeed/client/sources/kucoin/kucoin.go new file mode 100644 index 000000000..9d3750f62 --- /dev/null +++ b/daemons/pricefeed/client/sources/kucoin/kucoin.go @@ -0,0 +1,74 @@ +package kucoin + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// KucoinResponseBody defines the overall Kucoin response. +type KucoinResponseBody struct { + Code string `json:"code" validate:"required"` + Data KucoinResponseData `json:"data" validate:"required"` +} + +// KucoinResponseData defines the `data` field of Kucoin response. +type KucoinResponseData struct { + Tickers []KucoinTicker `json:"ticker" validate:"required"` +} + +// KucoinTicker is our representation of ticker information returned in Kucoin response. +// KucoinTicker implements interface `Ticker` in util.go. +type KucoinTicker struct { + Pair string `json:"symbol" validate:"required"` + AskPrice string `json:"sell" validate:"required,positive-float-string"` + BidPrice string `json:"buy" validate:"required,positive-float-string"` + LastPrice string `json:"last" validate:"required,positive-float-string"` +} + +// Ensure that KucoinTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*KucoinTicker)(nil) + +func (t KucoinTicker) GetPair() string { + return t.Pair +} + +func (t KucoinTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t KucoinTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t KucoinTicker) GetLastPrice() string { + return t.LastPrice +} + +// KucoinPriceFunction transforms an API response from Kucoin into a map of tickers to prices that have been +// shifted by a market specific exponent. +func KucoinPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body. + var kucoinResponseBody KucoinResponseBody + err = json.NewDecoder(response.Body).Decode(&kucoinResponseBody) + if err != nil { + return nil, nil, err + } + + if kucoinResponseBody.Code != "200000" { + return nil, nil, errors.New(`kucoin response code is not "200000"`) + } + + return price_function.GetMedianPricesFromTickers( + kucoinResponseBody.Data.Tickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/kucoin/kucoin_test.go b/daemons/pricefeed/client/sources/kucoin/kucoin_test.go new file mode 100644 index 000000000..937cbaf79 --- /dev/null +++ b/daemons/pricefeed/client/sources/kucoin/kucoin_test.go @@ -0,0 +1,238 @@ +package kucoin_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kucoin" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Kucoin. +const ( + BTCUSDC_TICKER = "BTC-USDT" + ETHUSDC_TICKER = "ETH-USDT" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestKucoinPriceFunction_Mixed(t *testing.T) { + // Test response strings. + var ( + btcTicker = pricefeed.ReadJsonTestFile(t, "btc_ticker.json") + ethTicker = pricefeed.ReadJsonTestFile(t, "eth_ticker.json") + + ResponseStringTemplate = `{"code":"200000","data":{"ticker":[%s]}}` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`{"code":"200000","data":{"ticker":[%s,%s]}}`, + btcTicker, ethTicker) + ) + + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"1646.22","sell":"1646.23","last":"1646.23",}`), + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to number askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"1646.22","sell":1646.23,"last":"1646.23"}`), + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "KucoinTicker.data.ticker.sell of type string"), + }, + "Unavailable - bid price is 0": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"0","sell":"1646.23","last":"1646.23"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'KucoinTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - ask price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"1646.22","sell":"-1646.23","last":"1646.23"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'KucoinTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - last price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"1646.22","sell":"1646.23","last":"-1646.23"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'KucoinTicker.LastPrice' Error:Field validation for " + + "'LastPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC-USDT"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC-USDT"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"1646.22","sell":"1646.23"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'KucoinTicker.LastPrice' Error:Field validation for 'LastPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - response status is not ok": { + responseJsonString: `{"code":"200001","data":{"ticker":[]}}`, + exponentMap: EthExponentMap, + expectedError: errors.New(`kucoin response code is not "200000"`), + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Mixed - missing btc response and has eth response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_646_230_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC-USDT"), + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH-USDT","buy":"1646","sell":"1646","last":"1646.23"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_646_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_500_850_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"BTC-USDT","buy":"25008.423951234","sell":"25008.5","last":"25008.4"}`), + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_500_842_395), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_500), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_500_850_000), + ETHUSDC_TICKER: uint64(1_646_230_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_646_230_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = kucoin.KucoinPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = kucoin.KucoinPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/kucoin/testdata/btc_ticker.json b/daemons/pricefeed/client/sources/kucoin/testdata/btc_ticker.json new file mode 100644 index 000000000..d96fe05e9 --- /dev/null +++ b/daemons/pricefeed/client/sources/kucoin/testdata/btc_ticker.json @@ -0,0 +1,18 @@ +{ + "symbol":"BTC-USDT", + "symbolName":"BTC-USDT", + "buy":"25008.4", + "sell":"25008.5", + "changeRate":"-0.0354", + "changePrice":"-918.7", + "high":"26096.2", + "low":"24821", + "vol":"5319.95816412", + "volValue":"135578356.948495567", + "last":"25008.5", + "averagePrice":"25932.02298664", + "takerFeeRate":"0.001", + "makerFeeRate":"0.001", + "takerCoefficient":"1", + "makerCoefficient":"1" +} diff --git a/daemons/pricefeed/client/sources/kucoin/testdata/eth_ticker.json b/daemons/pricefeed/client/sources/kucoin/testdata/eth_ticker.json new file mode 100644 index 000000000..b76c26b41 --- /dev/null +++ b/daemons/pricefeed/client/sources/kucoin/testdata/eth_ticker.json @@ -0,0 +1,18 @@ +{ + "symbol":"ETH-USDT", + "symbolName":"ETH-USDT", + "buy":"1646.22", + "sell":"1646.23", + "changeRate":"-0.0538", + "changePrice":"-93.64", + "high":"1750.37", + "low":"1630.7", + "vol":"48382.6736837", + "volValue":"81785550.1766849042", + "last":"1646.23", + "averagePrice":"1741.63106495", + "takerFeeRate":"0.001", + "makerFeeRate":"0.001", + "takerCoefficient":"1", + "makerCoefficient":"1" +} diff --git a/daemons/pricefeed/client/sources/mexc/exchange_query_details.go b/daemons/pricefeed/client/sources/mexc/exchange_query_details.go new file mode 100644 index 000000000..fe10a9ef6 --- /dev/null +++ b/daemons/pricefeed/client/sources/mexc/exchange_query_details.go @@ -0,0 +1,15 @@ +package mexc + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + MexcDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_MEXC, + Url: "https://www.mexc.com/open/api/v2/market/ticker", + PriceFunction: MexcPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/mexc/exchange_query_details_test.go b/daemons/pricefeed/client/sources/mexc/exchange_query_details_test.go new file mode 100644 index 000000000..44c2be3dc --- /dev/null +++ b/daemons/pricefeed/client/sources/mexc/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package mexc_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/mexc" +) + +func TestMexcUrl(t *testing.T) { + require.Equal(t, "https://www.mexc.com/open/api/v2/market/ticker", mexc.MexcDetails.Url) +} + +func TestMexcIsMultiMarket(t *testing.T) { + require.True(t, mexc.MexcDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/mexc/mexc.go b/daemons/pricefeed/client/sources/mexc/mexc.go new file mode 100644 index 000000000..a8890b655 --- /dev/null +++ b/daemons/pricefeed/client/sources/mexc/mexc.go @@ -0,0 +1,69 @@ +package mexc + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// MexcResponseBody defines the overall Mexc response. +type MexcResponseBody struct { + Code uint32 `json:"code" validate:"required"` + Tickers []MexcTicker `json:"data" validate:"required"` +} + +// MexcTicker is our representation of ticker information returned in Mexc response. +// MexcTicker implements interface `Ticker` in util.go. +type MexcTicker struct { + Pair string `json:"symbol" validate:"required"` + AskPrice string `json:"ask" validate:"required,positive-float-string"` + BidPrice string `json:"bid" validate:"required,positive-float-string"` + LastPrice string `json:"last" validate:"required,positive-float-string"` +} + +// Ensure that MexcTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*MexcTicker)(nil) + +func (t MexcTicker) GetPair() string { + return t.Pair +} + +func (t MexcTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t MexcTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t MexcTicker) GetLastPrice() string { + return t.LastPrice +} + +// MexcPriceFunction transforms an API response from Mexc into a map of tickers to prices that have been +// shifted by a market specific exponent. +func MexcPriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body. + var mexcResponseBody MexcResponseBody + err = json.NewDecoder(response.Body).Decode(&mexcResponseBody) + if err != nil { + return nil, nil, err + } + + if mexcResponseBody.Code != 200 { + return nil, nil, errors.New(`mexc response code is not 200`) + } + + return price_function.GetMedianPricesFromTickers( + mexcResponseBody.Tickers, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/mexc/mexc_test.go b/daemons/pricefeed/client/sources/mexc/mexc_test.go new file mode 100644 index 000000000..ff1584d70 --- /dev/null +++ b/daemons/pricefeed/client/sources/mexc/mexc_test.go @@ -0,0 +1,237 @@ +package mexc_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/mexc" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Mexc. +const ( + BTCUSDC_TICKER = "BTC_USDT" + ETHUSDC_TICKER = "ETH_USDT" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestMexcPriceFunction_Mixed(t *testing.T) { + // Test response strings. + var ( + btcTicker = pricefeed.ReadJsonTestFile(t, "btc_ticker.json") + ethTicker = pricefeed.ReadJsonTestFile(t, "eth_ticker.json") + + ResponseStringTemplate = `{"code":200,"data":[%s]}` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`{"code":200,"data":[%s,%s]}`, btcTicker, ethTicker) + ) + + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"1658.73","ask":"1658.74","last":"1658.63",}`), + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to number askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"1658.73","ask":1658.74,"last":"1658.63"}`), + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "MexcTicker.data.ask of type string"), + }, + "Unavailable - bid price is 0": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"0","ask":"1658.74","last":"1658.63"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'MexcTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - ask price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"1658.73","ask":"-1658.74","last":"1658.63"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'MexcTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - last price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"1658.73","ask":"1658.74","last":"-1658.63"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'MexcTicker.LastPrice' Error:Field validation for " + + "'LastPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USDT"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USDT"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","ask":"1658.74","last":"1658.63"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'MexcTicker.BidPrice' Error:Field validation for 'BidPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - response status is not 200": { + responseJsonString: `{"code":401,"data":[]}`, + exponentMap: EthExponentMap, + expectedError: errors.New(`mexc response code is not 200`), + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Mixed - missing btc response and has eth response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_658_730_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC_USDT"), + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"1658","ask":"1658","last":"1658.63"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_658_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_532_414_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"symbol":"ETH_USDT","bid":"1658.732942381293","ask":"1658.74","last":"1658.63"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_658_732_942), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_532), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_532_414_000), + ETHUSDC_TICKER: uint64(1_658_730_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_658_730_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = mexc.MexcPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = mexc.MexcPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/mexc/testdata/btc_ticker.json b/daemons/pricefeed/client/sources/mexc/testdata/btc_ticker.json new file mode 100644 index 000000000..e4bbcd116 --- /dev/null +++ b/daemons/pricefeed/client/sources/mexc/testdata/btc_ticker.json @@ -0,0 +1,13 @@ +{ + "symbol":"BTC_USDT", + "volume":"7795.767484", + "amount":"195483329.67", + "high":"25513.92", + "low":"24800.1", + "bid":"25324.13", + "ask":"25324.14", + "open":"25182.73", + "last":"25325.29", + "time":1686861300000, + "change_rate":"0.00566102" +} diff --git a/daemons/pricefeed/client/sources/mexc/testdata/eth_ticker.json b/daemons/pricefeed/client/sources/mexc/testdata/eth_ticker.json new file mode 100644 index 000000000..5e492d322 --- /dev/null +++ b/daemons/pricefeed/client/sources/mexc/testdata/eth_ticker.json @@ -0,0 +1,13 @@ +{ + "symbol":"ETH_USDT", + "volume":"39134.99214", + "amount":"64473733.72", + "high":"1672", + "low":"1627", + "bid":"1658.73", + "ask":"1658.74", + "open":"1655.44", + "last":"1658.63", + "time":1686861300000, + "change_rate":"0.00192698" +} diff --git a/daemons/pricefeed/client/sources/okx/exchange_query_details.go b/daemons/pricefeed/client/sources/okx/exchange_query_details.go new file mode 100644 index 000000000..2a8253ece --- /dev/null +++ b/daemons/pricefeed/client/sources/okx/exchange_query_details.go @@ -0,0 +1,15 @@ +package okx + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + OkxDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_OKX, + Url: "https://www.okx.com/api/v5/market/tickers?instType=SPOT", + PriceFunction: OkxPriceFunction, + IsMultiMarket: true, + } +) diff --git a/daemons/pricefeed/client/sources/okx/exchange_query_details_test.go b/daemons/pricefeed/client/sources/okx/exchange_query_details_test.go new file mode 100644 index 000000000..8d28e7b82 --- /dev/null +++ b/daemons/pricefeed/client/sources/okx/exchange_query_details_test.go @@ -0,0 +1,16 @@ +package okx_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/okx" +) + +func TestOkxUrl(t *testing.T) { + require.Equal(t, "https://www.okx.com/api/v5/market/tickers?instType=SPOT", okx.OkxDetails.Url) +} + +func TestOkxIsMultiMarket(t *testing.T) { + require.True(t, okx.OkxDetails.IsMultiMarket) +} diff --git a/daemons/pricefeed/client/sources/okx/okx.go b/daemons/pricefeed/client/sources/okx/okx.go new file mode 100644 index 000000000..697d0b5a8 --- /dev/null +++ b/daemons/pricefeed/client/sources/okx/okx.go @@ -0,0 +1,69 @@ +package okx + +import ( + "encoding/json" + "errors" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// OkxResponseBody defines the overall Okx response. +type OkxResponseBody struct { + Code string `json:"code" validate:"required"` + Tickers []OkxTicker `json:"data" validate:"required"` +} + +// OkxTicker is our representation of ticker information returned in Okx response. +// OkxTicker implements interface `Ticker` in util.go. +type OkxTicker struct { + Pair string `json:"instId" validate:"required"` + AskPrice string `json:"askPx" validate:"required,positive-float-string"` + BidPrice string `json:"bidPx" validate:"required,positive-float-string"` + LastPrice string `json:"last" validate:"required,positive-float-string"` +} + +// Ensure that OkxTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*OkxTicker)(nil) + +func (t OkxTicker) GetPair() string { + return t.Pair +} + +func (t OkxTicker) GetAskPrice() string { + return t.AskPrice +} + +func (t OkxTicker) GetBidPrice() string { + return t.BidPrice +} + +func (t OkxTicker) GetLastPrice() string { + return t.LastPrice +} + +// OkxPriceFunction transforms an API response from Okx into a map of tickers +// to prices that have been shifted by a market specific exponent. +func OkxPriceFunction( + response *http.Response, + marketPriceExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Unmarshal response body. + var okxResponseBody OkxResponseBody + err = json.NewDecoder(response.Body).Decode(&okxResponseBody) + if err != nil { + return nil, nil, err + } + + if okxResponseBody.Code != "0" { + return nil, nil, errors.New(`okx response code is not "0"`) + } + + return price_function.GetMedianPricesFromTickers( + okxResponseBody.Tickers, + marketPriceExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/okx/okx_test.go b/daemons/pricefeed/client/sources/okx/okx_test.go new file mode 100644 index 000000000..39f4739ff --- /dev/null +++ b/daemons/pricefeed/client/sources/okx/okx_test.go @@ -0,0 +1,237 @@ +package okx_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/okx" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testutil" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +// Test tickers for Okx. +const ( + BTCUSDC_TICKER = "BTC-USDT" + ETHUSDC_TICKER = "ETH-USDT" +) + +// Test exponent maps. +var ( + // Test exponent maps. + BtcExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + } + EthExponentMap = map[string]int32{ + ETHUSDC_TICKER: constants.EthUsdExponent, + } + BtcAndEthExponentMap = map[string]int32{ + BTCUSDC_TICKER: constants.BtcUsdExponent, + ETHUSDC_TICKER: constants.EthUsdExponent, + } +) + +func TestOkxPriceFunction_Mixed(t *testing.T) { + // Test response strings. + var ( + btcTicker = pricefeed.ReadJsonTestFile(t, "btc_ticker.json") + ethTicker = pricefeed.ReadJsonTestFile(t, "eth_ticker.json") + + ResponseStringTemplate = `{"code":"0","data":[%s]}` + BtcResponseString = fmt.Sprintf(ResponseStringTemplate, btcTicker) + EthResponseString = fmt.Sprintf(ResponseStringTemplate, ethTicker) + BtcAndEthResponseString = fmt.Sprintf(`{"code":"0","data":[%s,%s]}`, btcTicker, ethTicker) + ) + + tests := map[string]struct { + // parameters + responseJsonString string + exponentMap map[string]int32 + medianFunctionFails bool + + // expectations + expectedPriceMap map[string]uint64 + expectedUnavailableMap map[string]error + expectedError error + }{ + "Unavailable - invalid response": { + // Invalid due to trailing comma in JSON. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"1643.63","askPx":"1643.32","bidPx":"1643.31",}`), + exponentMap: EthExponentMap, + expectedError: errors.New("invalid character '}' looking for beginning of object key string"), + }, + "Unavailable - invalid type in response: number": { + // Invalid due to number askPrice when string was expected. + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":1643.63,"askPx":"1643.32","bidPx":"1643.31"}`), + exponentMap: EthExponentMap, + expectedError: errors.New("json: cannot unmarshal number into Go struct field " + + "OkxTicker.data.last of type string"), + }, + "Unavailable - bid price is 0": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"1643.63","askPx":"1643.32","bidPx":"0"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'OkxTicker.BidPrice' Error:Field validation for " + + "'BidPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - ask price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"1643.63","askPx":"-1643.32","bidPx":"1643.31"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'OkxTicker.AskPrice' Error:Field validation for " + + "'AskPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - last price is negative": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"-1643.63","askPx":"1643.32","bidPx":"1643.31"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New("Key: 'OkxTicker.LastPrice' Error:Field validation for " + + "'LastPrice' failed on the 'positive-float-string' tag"), + }, + }, + "Unavailable - empty response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, `{}`), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC-USDT"), + }, + }, + "Unavailable - empty list response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, ``), + exponentMap: BtcExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC-USDT"), + }, + }, + "Unavailable - incomplete response": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"1643.63","askPx":"1643.32"}`), + exponentMap: EthExponentMap, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + ETHUSDC_TICKER: errors.New( + "Key: 'OkxTicker.BidPrice' Error:Field validation for 'BidPrice' failed on the 'required' tag", + ), + }, + }, + "Failure - response status is not ok": { + responseJsonString: `{"code":"1","data":[]}`, + exponentMap: EthExponentMap, + expectedError: errors.New(`okx response code is not "0"`), + }, + "Failure - overflow due to massively negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{BTCUSDC_TICKER: -3000}, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("value overflows uint64"), + }, + }, + "Failure - medianization error": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + medianFunctionFails: true, + expectedPriceMap: make(map[string]uint64), + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: testutil.MedianizationError, + }, + }, + "Mixed - missing btc response and has eth response": { + responseJsonString: EthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_643_320_000), + }, + expectedUnavailableMap: map[string]error{ + BTCUSDC_TICKER: errors.New("no listing found for ticker BTC-USDT"), + }, + }, + "Success - integers": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"1643","askPx":"1643","bidPx":"1643.31"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_643_000_000), + }, + }, + "Success - negative exponent": { + responseJsonString: BtcResponseString, + exponentMap: BtcExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_496_920_000), + }, + }, + "Success - decimals beyond supported precision ignored": { + responseJsonString: fmt.Sprintf(ResponseStringTemplate, + `{"instId":"ETH-USDT","last":"1643.63","askPx":"1643.3294238412","bidPx":"1643.31"}`), + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_643_329_423), + }, + }, + "Success - positive exponent": { + responseJsonString: BtcResponseString, + exponentMap: map[string]int32{ + BTCUSDC_TICKER: 1, + }, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_496), + }, + }, + "Success - two tickers in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: BtcAndEthExponentMap, + expectedPriceMap: map[string]uint64{ + BTCUSDC_TICKER: uint64(2_496_920_000), + ETHUSDC_TICKER: uint64(1_643_320_000), + }, + }, + "Success - one ticker in request, two tickers in response": { + responseJsonString: BtcAndEthResponseString, + exponentMap: EthExponentMap, + expectedPriceMap: map[string]uint64{ + ETHUSDC_TICKER: uint64(1_643_320_000), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response := testutil.CreateResponseFromJson(tc.responseJsonString) + + var prices map[string]uint64 + var unavailable map[string]error + var err error + if tc.medianFunctionFails { + prices, unavailable, err = okx.OkxPriceFunction(response, tc.exponentMap, testutil.MedianErr) + } else { + prices, unavailable, err = okx.OkxPriceFunction(response, tc.exponentMap, lib.Median[uint64]) + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailable) + } else { + require.Equal(t, tc.expectedPriceMap, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailableMap, unavailable) + require.NoError(t, err) + } + }) + } +} diff --git a/daemons/pricefeed/client/sources/okx/testdata/btc_ticker.json b/daemons/pricefeed/client/sources/okx/testdata/btc_ticker.json new file mode 100644 index 000000000..51309a142 --- /dev/null +++ b/daemons/pricefeed/client/sources/okx/testdata/btc_ticker.json @@ -0,0 +1,18 @@ +{ + "instType":"SPOT", + "instId":"BTC-USDT", + "last":"24969.2", + "lastSz":"0.00001143", + "askPx":"24969.3", + "askSz":"1.41619718", + "bidPx":"24969.2", + "bidSz":"0.95523449", + "open24h":"25959.6", + "high24h":"26098.3", + "low24h":"24783", + "volCcy24h":"448381683.561416977", + "vol24h":"17826.41028782", + "ts":"1686844335704", + "sodUtc0":"25129.1", + "sodUtc8":"25986.7" +} diff --git a/daemons/pricefeed/client/sources/okx/testdata/eth_ticker.json b/daemons/pricefeed/client/sources/okx/testdata/eth_ticker.json new file mode 100644 index 000000000..69de7365a --- /dev/null +++ b/daemons/pricefeed/client/sources/okx/testdata/eth_ticker.json @@ -0,0 +1,18 @@ +{ + "instType":"SPOT", + "instId":"ETH-USDT", + "last":"1643.63", + "lastSz":"8.134068", + "askPx":"1643.32", + "askSz":"0.25", + "bidPx":"1643.31", + "bidSz":"4.895967", + "open24h":"1737.94", + "high24h":"1747.39", + "low24h":"1621.86", + "volCcy24h":"367018643.63073004", + "vol24h":"220409.969171", + "ts":"1686844336407", + "sodUtc0":"1651.07", + "sodUtc8":"1741.11" +} diff --git a/daemons/pricefeed/client/sources/test_fixed_price_exchange/exchange_query_details.go b/daemons/pricefeed/client/sources/test_fixed_price_exchange/exchange_query_details.go new file mode 100644 index 000000000..aaa1e2b39 --- /dev/null +++ b/daemons/pricefeed/client/sources/test_fixed_price_exchange/exchange_query_details.go @@ -0,0 +1,33 @@ +package test_fixed_price_exchange + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// Fixed prices for BTC-USD, ETH-USD, SOL-USD +const ( + BTC_USD_PRICE = 50000 + ETH_USD_PRICE = 4000 + SOL_USD_PRICE = 100 +) + +type FixedPriceExchangeParams struct { + BTCUSDPrice float64 + ETHUSDPrice float64 + SOLUSDPrice float64 +} + +var ( + TestFixedPriceExchangeParams = FixedPriceExchangeParams{ + BTCUSDPrice: BTC_USD_PRICE, + ETHUSDPrice: ETH_USD_PRICE, + SOLUSDPrice: SOL_USD_PRICE, + } + TestFixedPriceExchangeDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE, + Url: "https://jsonplaceholder.typicode.com/users", + PriceFunction: FixedExchangePriceFunction, + IsMultiMarket: false, + } +) diff --git a/daemons/pricefeed/client/sources/test_fixed_price_exchange/fixed_price_function.go b/daemons/pricefeed/client/sources/test_fixed_price_exchange/fixed_price_function.go new file mode 100644 index 000000000..de02a457e --- /dev/null +++ b/daemons/pricefeed/client/sources/test_fixed_price_exchange/fixed_price_function.go @@ -0,0 +1,56 @@ +package test_fixed_price_exchange + +import ( + "fmt" + "net/http" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +type FixedPriceTicker struct { + Pair string + Price string +} + +var _ price_function.Ticker = (*FixedPriceTicker)(nil) + +func (t FixedPriceTicker) GetPair() string { + return t.Pair +} + +func (t FixedPriceTicker) GetAskPrice() string { + return t.Price +} + +func (t FixedPriceTicker) GetBidPrice() string { + return t.Price +} + +func (t FixedPriceTicker) GetLastPrice() string { + return t.Price +} + +func FixedExchangePriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + btcTicker := FixedPriceTicker{ + Pair: "BTC-USD", + Price: fmt.Sprintf("%f", TestFixedPriceExchangeParams.BTCUSDPrice), + } + ethTicker := FixedPriceTicker{ + Pair: "ETH-USD", + Price: fmt.Sprintf("%f", TestFixedPriceExchangeParams.ETHUSDPrice), + } + solTicker := FixedPriceTicker{ + Pair: "SOL-USD", + Price: fmt.Sprintf("%f", TestFixedPriceExchangeParams.SOLUSDPrice), + } + return price_function.GetMedianPricesFromTickers( + []FixedPriceTicker{btcTicker, ethTicker, solTicker}, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/test_volatile_exchange/exchange_query_details.go b/daemons/pricefeed/client/sources/test_volatile_exchange/exchange_query_details.go new file mode 100644 index 000000000..7bd941d81 --- /dev/null +++ b/daemons/pricefeed/client/sources/test_volatile_exchange/exchange_query_details.go @@ -0,0 +1,29 @@ +package test_volatile_exchange + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +const SECONDS_IN_DAY = 24 * 60 * 60 + +type VolatileExchangeParams struct { + AveragePrice float64 + Amplitude float64 + Frequency float64 +} + +// Test Exchange used for testing purposes. +var ( + TestVolatileExchangeParams = VolatileExchangeParams{ + AveragePrice: 100, + Amplitude: 0.95, + Frequency: 1, + } + TestVolatileExchangeDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_TEST_VOLATILE_EXCHANGE, + Url: "https://jsonplaceholder.typicode.com/users", + PriceFunction: VolatileExchangePriceFunction, + IsMultiMarket: false, + } +) diff --git a/daemons/pricefeed/client/sources/test_volatile_exchange/volatile_price_function.go b/daemons/pricefeed/client/sources/test_volatile_exchange/volatile_price_function.go new file mode 100644 index 000000000..6c2b30acf --- /dev/null +++ b/daemons/pricefeed/client/sources/test_volatile_exchange/volatile_price_function.go @@ -0,0 +1,87 @@ +package test_volatile_exchange + +import ( + "fmt" + "math" + "net/http" + "time" + + price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// VolatileExchangeTicker is our representation of ticker information for a test market. +// It implements interface `Ticker` in util.go. +type VolatileExchangeTicker struct { + Pair string + Price string +} + +// Ensure that VolatileExchangeTicker implements the Ticker interface at compile time. +var _ price_function.Ticker = (*VolatileExchangeTicker)(nil) + +func (t VolatileExchangeTicker) GetPair() string { + return t.Pair +} + +func (t VolatileExchangeTicker) GetAskPrice() string { + return t.Price +} + +func (t VolatileExchangeTicker) GetBidPrice() string { + return t.Price +} + +func (t VolatileExchangeTicker) GetLastPrice() string { + return t.Price +} + +// VolatileExchangePriceFunction generates a time-based price value. The value follows a cosine wave +// function, but that includes jumps from the lowest value to the highest value (and vice versa) +// once per period. The general formula is written below. +// - PRICE = AVERAGE * (1 + AMPLITUDE * WAVE_VALUE) +// - WAVE_VALUE = math.Cos(RADIANS) +// - RADIANS = (PHASE <= 0.5 ? PHASE * 4 : PHASE * 4 - 1) * math.Pi +// - PHASE = (FREQUENCY * UNIX_SECONDS / SECONDS_IN_DAY) % 1 +// The following values are parametrized in `VolatileExchangeParams`: +// - AVERAGE, AMPLITUDE, FREQUENCY +func VolatileExchangePriceFunction( + response *http.Response, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Calculate the phase, how far in the period we are. + // The phase is a value that goes from 0 to 1 over the timespan of (1 day / frequency). + phase := math.Mod( + TestVolatileExchangeParams.Frequency* + float64(time.Now().Unix())/ + float64(SECONDS_IN_DAY), + 1, + ) + + // Next we get the radians. Over each period, we want the price to "jump" from the max to the min + // and vice versa. Otherwise the price should move smoothly between min and max. + // So we want the first half of the period to move from 0-2 pi radians, + // and the second half of the period to move from 1-3 pi radians. + radMultiplier := phase * float64(4) + if phase > 0.5 { + radMultiplier -= float64(1) + } + radians := radMultiplier * float64(math.Pi) + + // Next we get the final wave value in the range from -1 to 1 based on the radians. + waveValue := math.Cos(radians) + + // The price value is centered around `AveragePrice` with an amplitude of `Amplitude`. + price := float64(TestVolatileExchangeParams.AveragePrice) * (1 + TestVolatileExchangeParams.Amplitude*waveValue) + + volatile_exchange_ticker := VolatileExchangeTicker{ + Pair: "TEST-USD", + Price: fmt.Sprintf("%f", price), + } + return price_function.GetMedianPricesFromTickers( + []VolatileExchangeTicker{volatile_exchange_ticker}, + tickerToExponent, + resolver, + ) +} diff --git a/daemons/pricefeed/client/sources/testexchange/exchange_query_details.go b/daemons/pricefeed/client/sources/testexchange/exchange_query_details.go new file mode 100644 index 000000000..03c5e9d3e --- /dev/null +++ b/daemons/pricefeed/client/sources/testexchange/exchange_query_details.go @@ -0,0 +1,20 @@ +package testexchange + +import ( + "fmt" + + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/coinbase_pro" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// Exchange used for testing purposes. We'll reuse the CoinbasePro price function. +var ( + TestExchangeHost = "test.exchange" + TestExchangePort = "9888" + TestExchangeDetails = types.ExchangeQueryDetails{ + Exchange: exchange_common.EXCHANGE_ID_TEST_EXCHANGE, + Url: fmt.Sprintf("http://%s:%s/ticker?symbol=$", TestExchangeHost, TestExchangePort), + PriceFunction: coinbase_pro.CoinbaseProPriceFunction, + } +) diff --git a/daemons/pricefeed/client/sources/testutil/test_util.go b/daemons/pricefeed/client/sources/testutil/test_util.go new file mode 100644 index 000000000..8b083a158 --- /dev/null +++ b/daemons/pricefeed/client/sources/testutil/test_util.go @@ -0,0 +1,24 @@ +package testutil + +import ( + "bytes" + "errors" + "io" + "net/http" +) + +var ( + // MedianizationError is a generic error returned when the below MedianErr method is called. + MedianizationError = errors.New("Failed to get median") +) + +// MedianErr mocks the median function and returns an error. +func MedianErr(a []uint64) (uint64, error) { + return uint64(0), MedianizationError +} + +// CreateResponseFromJson creates a http response from a json string. +func CreateResponseFromJson(m string) *http.Response { + jsonBlob := bytes.NewReader([]byte(m)) + return &http.Response{Body: io.NopCloser(jsonBlob)} +} diff --git a/daemons/pricefeed/client/sources/util.go b/daemons/pricefeed/client/sources/util.go new file mode 100644 index 000000000..a24f10b11 --- /dev/null +++ b/daemons/pricefeed/client/sources/util.go @@ -0,0 +1,249 @@ +package sources + +import ( + "errors" + "fmt" + "math/big" + "regexp" + "strconv" + + "github.com/go-playground/validator/v10" + "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/lib" +) + +var ( + apiResponseValidator *validator.Validate + + // exchangeSideErrorResponsePatterns captures a list of error strings patterns are generated by exchange APIs. + // These errors are not attributable to the exchange by http status code, but rather by the response body. + exchangeSideErrorResponsePatterns = []string{ + // An error that occurs server-side on the exchange. These are generic error strings that are + // percolated up by go's http2 library. + // nolint:lll + // See https://stackoverflow.com/questions/45209168/http2-server-sent-goaway-and-closed-the-connection-laststreamid-1999 + `http2: server sent GOAWAY and closed the connection`, + + // http2 error: Server closed connection. Could sometimes be due to rate limiting, as not all exchanges return + // 429s. + "http2: client connection force closed via ClientConn.Close", + + // This can sometimes occur with TLS errors, or could arise as a result of gzip decompression by the go + // http2 library. + "unexpected EOF", + + // Existing error response identified as an internal server error by status code. + "Unexpected response status code of: (\\d+)", + + "Failed to get entities from store", + + "request timeout", + + // "internal error", "Internal error", "INTERNAL_ERROR", "SYS_ERROR" + "([Ii]nternal error|INTERNAL_ERROR|SYS_ERROR)", + } +) + +// IsGenericExchangeError returns true if the error message has been positively identified as being due to an exchange +// side error. These errors could occur on any exchange. +func IsGenericExchangeError(err error) bool { + for _, exchangeSideErrorResponse := range exchangeSideErrorResponsePatterns { + if match, err := regexp.Match(exchangeSideErrorResponse, []byte(err.Error())); err == nil && match { + return true + } + } + return false +} + +// validatePositiveNumericString is a custom validation function that ensures a particular string field in +// a struct being validated can be parsed into a positive-valued float. We register this function in order +// to ensure that returned numeric string values in the Kraken response do not represent zero or negative numbers. +// To see where this is used, note the `validate:"positive-float-string"` struct tag in the KrakenTickerResult. +func validatePositiveNumericString(fl validator.FieldLevel) bool { + val, err := strconv.ParseFloat(fl.Field().String(), 64) + if err != nil { + return false + } + return val > 0 +} + +// GetApiResponseValidator returns a validator with custom logic registered to validate fields returned by +// various exchange API responses. +func GetApiResponseValidator() (*validator.Validate, error) { + if apiResponseValidator == nil { + validate := validator.New() + err := validate.RegisterValidation("positive-float-string", validatePositiveNumericString) + if err != nil { + return nil, fmt.Errorf("API response validation internal error (%w)", err) + } + apiResponseValidator = validate + } + return apiResponseValidator, nil +} + +// GetOnlyTickerAndExponent returns the only ticker and exponent in the provided +// `tickerToExponent` map. If the map contains more than one key, an error is returned. +func GetOnlyTickerAndExponent( + tickerToExponent map[string]int32, + exchange string, +) (string, int32, error) { + // Verify exactly one market is expected from the response. + if len(tickerToExponent) != 1 { + return "", 0, fmt.Errorf( + "Invalid market price exponent map for %v price function of length: %v, expected length 1", + exchange, + len(tickerToExponent), + ) + } + + // Get ticker and value of exponent. + var ticker string + var exponent int32 + // Set `ticker` and `exponent` explicitly from the for loop. + // tickerToExponent has only one entry so the for loop only runs once. + for ticker, exponent = range tickerToExponent { + } + + return ticker, exponent, nil +} + +// GetUint64MedianFromReverseShiftedBigFloatValues shifts all values in a slice of floats by an +// exponent, converts the shifted slice values to uint64 and then returns the median of the slice. +// 1) Verify length of slice is > 0. +// 2) Reverse shift big float price values by the exponent for the market. +// 3) Convert big float values to uint64 values. +// 4) Get the median value from the uint64 price values and return. +func GetUint64MedianFromReverseShiftedBigFloatValues( + bigFloatSlice []*big.Float, + exponent int32, + resolver func([]uint64) (uint64, error), +) (uint64, error) { + // 1) Verify length of slice is > 0. + if len(bigFloatSlice) == 0 { + return 0, errors.New("Invalid input: big float slice must contain values to medianize") + } + + // 2) Reverse shift big float price values by the exponent for the market. + updatedBigFloatSlice := reverseShiftBigFloatSlice(bigFloatSlice, exponent) + + // 3) Convert big float values to uint64 values. + uint64Slice, err := lib.ConvertBigFloatSliceToUint64Slice(updatedBigFloatSlice) + if err != nil { + return 0, err + } + + // 4) Get the median value from the uint64 price values and return. + return resolver(uint64Slice) +} + +// reverseShiftBigFloatSlice shifts the given floats by exponent in the reverse direction. +// If the exponent is 0, then do nothing (i.e. `123.456` -> `123.456`) +// If the exponent is positive, then shift to the right (i.e. exponent = 1, `123.456` -> `12.3456`) +// If the exponent is negative, then shift to the left (i.e. exponent = -1, `123.456` -> `1234.56`) +func reverseShiftBigFloatSlice( + values []*big.Float, + exponent int32, +) []*big.Float { + unsignedExponent := lib.AbsInt32(exponent) + + pow10 := new(big.Float).SetInt(lib.BigPow10(uint64(unsignedExponent))) + updatedValues := make([]*big.Float, 0, len(values)) + for _, value := range values { + updatedValues = append(updatedValues, reverseShiftFloatWithPow10(value, pow10, exponent)) + } + return updatedValues +} + +func reverseShiftFloatWithPow10(value *big.Float, pow10 *big.Float, exponent int32) *big.Float { + if exponent == 0 { + return value + } else if exponent > 0 { + return new(big.Float).Quo(value, pow10) + } else { // exponent < 0 + return new(big.Float).Mul(value, pow10) + } +} + +// Ticker encodes a ticker response returned by an exchange API. It contains accessors for the ticker's +// ask price, bid price, and last price, which are medianized to compute an exchange price. +type Ticker interface { + GetPair() string + GetAskPrice() string + GetBidPrice() string + GetLastPrice() string +} + +// GetMedianPricesFromTickers processes through a list of `tickers` and calculates a median price (from +// `LastPrice`, `AskPrice`, and `BidPrice`) for each ticker in `tickerToExponent` and marks a ticker +// as unavailable if it's not present in `tickers` or its ticker's validation or calculation fails. +func GetMedianPricesFromTickers[T Ticker]( + tickers []T, + tickerToExponent map[string]int32, + resolver types.Resolver, +) (tickerToPrice map[string]uint64, unavailableTickers map[string]error, err error) { + // Create API response validator, if not already. + if apiResponseValidator == nil { + apiResponseValidator, err = GetApiResponseValidator() + if err != nil { + return nil, nil, fmt.Errorf("Error creating API response validator (%w)", err) + } + } + + tickerToPrice = make(map[string]uint64, len(tickerToExponent)) + unavailableTickers = make(map[string]error) + + // Iterate through every ticker in response and calculate median prices for requested + // tickers (the ones present in `tickerToExponent`). + for _, ticker := range tickers { + tickerPair := ticker.GetPair() + if exponent, exists := tickerToExponent[tickerPair]; exists { + // Validate ticker info. + err = apiResponseValidator.Struct(ticker) + // If validation failed, mark tickerPair as unavailable with validation error. + if err != nil { + unavailableTickers[tickerPair] = err + continue + } + // Get big float values from prices in response. + bigFloatSlice, err := lib.ConvertStringSliceToBigFloatSlice( + []string{ticker.GetAskPrice(), ticker.GetBidPrice(), ticker.GetLastPrice()}) + // If unsuccessful, mark ticker as unavailable with corresponding error. + if err != nil { + unavailableTickers[tickerPair] = err + continue + } + + // Get the median uint64 value from the slice of big float price values. + medianPrice, err := GetUint64MedianFromReverseShiftedBigFloatValues( + bigFloatSlice, + exponent, + resolver, + ) + // If unsuccessful, mark tickerPair as unavailable with corresponding error. + // Otherwise, store median price for corresponding tickerPair. + if err != nil { + unavailableTickers[tickerPair] = err + continue + } else { + tickerToPrice[tickerPair] = medianPrice + } + } + } + + // Iterate through every requested ticker and mark as unavailable if it wasn't + // seen / processed in above loop. + for ticker := range tickerToExponent { + _, priceCalculationSucceeded := tickerToPrice[ticker] + _, priceCalculationErrored := unavailableTickers[ticker] + if !priceCalculationSucceeded && !priceCalculationErrored { + unavailableTickers[ticker] = fmt.Errorf("no listing found for ticker %v", ticker) + } + } + + return tickerToPrice, unavailableTickers, nil +} + +// ConvertFloat64ToString converts a `float64` to `string`. +func ConvertFloat64ToString(num float64) string { + return strconv.FormatFloat(num, 'f', -1, 64) +} diff --git a/daemons/pricefeed/client/sources/util_test.go b/daemons/pricefeed/client/sources/util_test.go new file mode 100644 index 000000000..016e3e448 --- /dev/null +++ b/daemons/pricefeed/client/sources/util_test.go @@ -0,0 +1,433 @@ +package sources + +import ( + "errors" + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/lib" +) + +const ( + ETHUSDC = "ETHUSDC" + BTCUSDC = "BTCUSDC" + deltaPrecision = 0.000000001 +) + +var ( + positiveTagValidationError = errors.New( + "Key: 'TestPositiveValidation.PositiveFloatString' Error:Field validation for 'PositiveFloatString' " + + "failed on the 'positive-float-string' tag", + ) +) + +func TestIsExchangeError_Mixed(t *testing.T) { + tests := map[string]struct { + err error + isExchangeError bool + }{ + "Exchange Error - server sent GOAWAY": { + err: fmt.Errorf(`http2: server sent GOAWAY and closed the connection`), + isExchangeError: true, + }, + "Exchange Error - server sent GOAWAY with extra text": { + err: fmt.Errorf(`http2: server sent GOAWAY and closed the connection blah blah blah`), + isExchangeError: true, + }, + "Exchange Error - internal error": { + err: fmt.Errorf("internal error: something went wrong"), + isExchangeError: true, + }, + "Exchange Error - Internal error": { + err: fmt.Errorf("Internal error: something went wrong"), + isExchangeError: true, + }, + "Exchange Error - INTERNAL_ERROR": { + err: fmt.Errorf("INTERNAL_ERROR: something went wrong"), + isExchangeError: true, + }, + "Exchange Error - generic": { + err: fmt.Errorf("Unexpected response status code of: 5"), + isExchangeError: true, + }, + "Not exchange error": { + err: fmt.Errorf("some other error"), + isExchangeError: false, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.isExchangeError, IsGenericExchangeError(tc.err)) + }) + } +} + +func TestGetApiResponseValidator_validatePositiveNumericString_Mixed(t *testing.T) { + tests := map[string]struct { + testValue string + expectedError error + }{ + "Success - canonical float": { + testValue: "12345.6", + }, + "Failure - negative float": { + testValue: "-12345.6", + expectedError: positiveTagValidationError, + }, + "Failure - empty string": { + testValue: "", + expectedError: positiveTagValidationError, + }, + "Failure - text": { + testValue: "cat", + expectedError: positiveTagValidationError, + }, + } + + type TestPositiveValidation struct { + PositiveFloatString string `validate:"positive-float-string"` + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + validator, err := GetApiResponseValidator() + require.Nil(t, err) + err = validator.Struct(TestPositiveValidation{ + PositiveFloatString: tc.testValue, + }) + if tc.expectedError == nil { + require.Nil(t, err) + } else { + require.NotNil(t, err) + require.EqualError(t, tc.expectedError, err.Error()) + } + }) + } +} + +func TestGetOnlyTickerAndExponent(t *testing.T) { + tests := map[string]struct { + //parameters + tickerToExponent map[string]int32 + exchange string + + // expectations + expectedTicker string + expectedExponent int32 + expectedError error + }{ + "Success - isPositive = true and exchange = Binance": { + tickerToExponent: map[string]int32{ + ETHUSDC: 6, + }, + exchange: exchange_common.EXCHANGE_ID_BINANCE, + expectedTicker: ETHUSDC, + expectedExponent: 6, + }, + + "Success - isNegative = false and exchange = Bitfinex": { + tickerToExponent: map[string]int32{ + ETHUSDC: -6, + }, + exchange: exchange_common.EXCHANGE_ID_BITFINEX, + expectedTicker: ETHUSDC, + expectedExponent: -6, + }, + "Failure - no exponents": { + tickerToExponent: map[string]int32{}, + exchange: exchange_common.EXCHANGE_ID_BINANCE, + expectedError: errors.New( + "Invalid market price exponent map for Binance price function of length: 0, expected length 1", + ), + }, + + "Failure - too many exponents": { + tickerToExponent: map[string]int32{ + ETHUSDC: -6, + BTCUSDC: -8, + }, + exchange: exchange_common.EXCHANGE_ID_BITFINEX, + expectedError: errors.New( + "Invalid market price exponent map for Bitfinex price function of length: 2, expected length 1", + ), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ticker, + exponent, + err := GetOnlyTickerAndExponent( + tc.tickerToExponent, + tc.exchange, + ) + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + + require.Equal(t, tc.expectedTicker, ticker) + require.Equal(t, tc.expectedExponent, exponent) + } else { + require.NoError(t, err) + + require.Equal(t, tc.expectedTicker, ticker) + require.Equal(t, tc.expectedExponent, exponent) + } + }) + } +} + +func TestGetUint64MedianFromShiftedBigFloatValues(t *testing.T) { + tests := map[string]struct { + // parameters + bigFloatSlice []*big.Float + exponent int32 + + // expectations + expectedMedianValue uint64 + expectedError error + }{ + "Success - isPositive = false": { + bigFloatSlice: []*big.Float{ + new(big.Float).SetFloat64(100.001), + new(big.Float).SetFloat64(300.001), + new(big.Float).SetFloat64(200.022), + }, + exponent: -2, + expectedMedianValue: uint64(200_02), + }, + "Success - isPositive = true": { + bigFloatSlice: []*big.Float{ + new(big.Float).SetFloat64(100.001), + new(big.Float).SetFloat64(300.001), + new(big.Float).SetFloat64(200.002), + }, + exponent: 2, + expectedMedianValue: uint64(2), + }, + "Success - one value": { + bigFloatSlice: []*big.Float{ + new(big.Float).SetFloat64(100.0002), + }, + exponent: 0, + expectedMedianValue: uint64(100), + }, + "Failure - empty bigFloatSlice": { + bigFloatSlice: []*big.Float{}, + exponent: 0, + expectedError: errors.New( + "Invalid input: big float slice must contain values to medianize", + ), + }, + "Failure - underflow": { + bigFloatSlice: []*big.Float{ + new(big.Float).SetFloat64(-100), + }, + exponent: 0, + expectedError: errors.New( + "value underflows uint64", + ), + }, + "Failure - overflow": { + bigFloatSlice: []*big.Float{ + new(big.Float).SetFloat64(100), + }, + exponent: -1000, + expectedError: errors.New( + "value overflows uint64", + ), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + median, err := GetUint64MedianFromReverseShiftedBigFloatValues( + tc.bigFloatSlice, + tc.exponent, + lib.Median[uint64], + ) + + if tc.expectedError != nil { + require.Equal(t, uint64(0), median) + require.EqualError(t, err, tc.expectedError.Error()) + } else { + require.Equal(t, tc.expectedMedianValue, median) + require.NoError(t, err) + } + }) + } +} + +func TestReverseShiftBigFloatWithPow10(t *testing.T) { + tests := map[string]struct { + // parameters + floatValue *big.Float + exponent int32 + + // expectations + expectedUpdatedFloatValue *big.Float + }{ + "Success with negative exponent": { + floatValue: new(big.Float).SetPrec(64).SetFloat64(100.123), + exponent: -3, + expectedUpdatedFloatValue: new(big.Float).SetPrec(64).SetFloat64(100_123), + }, + "Success with positive exponent": { + floatValue: new(big.Float).SetPrec(64).SetFloat64(100.1), + exponent: 1, + expectedUpdatedFloatValue: new(big.Float).SetPrec(64).SetFloat64(10.01), + }, + "Success with exponent of 0": { + floatValue: new(big.Float).SetPrec(64).SetFloat64(100), + exponent: 0, + expectedUpdatedFloatValue: new(big.Float).SetPrec(64).SetFloat64(100), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + unsignedExponent := lib.AbsInt32(tc.exponent) + + pow10 := new(big.Float).SetInt(lib.BigPow10(uint64(unsignedExponent))) + + updatedFloatValue := reverseShiftFloatWithPow10( + tc.floatValue, + pow10, + tc.exponent, + ) + + require.InDeltaSlice( + t, + bigSliceToFloatSlice([]*big.Float{tc.expectedUpdatedFloatValue}), + bigSliceToFloatSlice([]*big.Float{updatedFloatValue}), + deltaPrecision, + ) + }) + } +} + +func TestReverseShiftBigFloatSlice(t *testing.T) { + tests := map[string]struct { + // parameters + floatValues []*big.Float + exponent int32 + + // expectations + expectedUpdatedFloatValues []*big.Float + }{ + "Success with empty floatValues": { + floatValues: []*big.Float{}, + exponent: -3, + expectedUpdatedFloatValues: []*big.Float{}, + }, + "Success with negative exponent": { + floatValues: []*big.Float{new(big.Float).SetPrec(64).SetFloat64(100.123)}, + exponent: -3, + expectedUpdatedFloatValues: []*big.Float{new(big.Float).SetPrec(64).SetFloat64(100_123)}, + }, + "Success with multiple values and a negative exponent": { + floatValues: []*big.Float{new(big.Float).SetFloat64(100.122), new(big.Float).SetFloat64(2)}, + exponent: -3, + expectedUpdatedFloatValues: []*big.Float{ + new(big.Float).SetPrec(64).SetFloat64(100_122), + new(big.Float).SetPrec(64).SetFloat64(2_000), + }, + }, + "Success with positive exponent": { + floatValues: []*big.Float{new(big.Float).SetPrec(64).SetFloat64(100.1)}, + exponent: 1, + expectedUpdatedFloatValues: []*big.Float{new(big.Float).SetPrec(64).SetFloat64(10.01)}, + }, + "Success with multiple values and a positive exponent": { + floatValues: []*big.Float{ + new(big.Float).SetPrec(64).SetFloat64(100), + new(big.Float).SetPrec(64).SetFloat64(20), + }, + exponent: 1, + expectedUpdatedFloatValues: []*big.Float{ + new(big.Float).SetPrec(64).SetFloat64(10), + new(big.Float).SetPrec(64).SetFloat64(2), + }, + }, + "Success with exponent of 0": { + floatValues: []*big.Float{new(big.Float).SetPrec(64).SetFloat64(100)}, + exponent: 0, + expectedUpdatedFloatValues: []*big.Float{new(big.Float).SetPrec(64).SetFloat64(100)}, + }, + "Success with multiple values and an exponent of 0": { + floatValues: []*big.Float{ + new(big.Float).SetPrec(64).SetFloat64(100.1), + new(big.Float).SetPrec(64).SetFloat64(20.0000012), + }, + exponent: 0, + expectedUpdatedFloatValues: []*big.Float{ + new(big.Float).SetPrec(64).SetFloat64(100.1), + new(big.Float).SetPrec(64).SetFloat64(20.0000012), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + updatedFloatValues := reverseShiftBigFloatSlice( + tc.floatValues, + tc.exponent, + ) + + require.InDeltaSlice( + t, + bigSliceToFloatSlice(tc.expectedUpdatedFloatValues), + bigSliceToFloatSlice(updatedFloatValues), + deltaPrecision, + ) + }) + } +} + +func TestConvertFloat64ToString(t *testing.T) { + tests := map[string]struct { + // parameters + float64Value float64 + + // expectations + expectedFloat64String string + }{ + "Success with low precision number": { + float64Value: float64(1.23), + expectedFloat64String: "1.23", + }, + "Success with a high precision number": { + float64Value: float64(0.12345678987654321), + expectedFloat64String: "0.12345678987654321", + }, + "Success with a large positive number": { + float64Value: float64(123456789.12345), + expectedFloat64String: "123456789.12345", + }, + "Success with a large negative number": { + float64Value: float64(-123456789.12345), + expectedFloat64String: "-123456789.12345", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + float64String := ConvertFloat64ToString(tc.float64Value) + + require.Equal(t, tc.expectedFloat64String, float64String) + }) + } +} + +func bigSliceToFloatSlice(bigFloat []*big.Float) []float64 { + floatSlice := make([]float64, 0, len(bigFloat)) + for _, val := range bigFloat { + floatVal, _ := val.Float64() + floatSlice = append(floatSlice, floatVal) + } + + return floatSlice +} diff --git a/daemons/pricefeed/client/subtask_runner.go b/daemons/pricefeed/client/subtask_runner.go new file mode 100644 index 000000000..0b0992f1d --- /dev/null +++ b/daemons/pricefeed/client/subtask_runner.go @@ -0,0 +1,320 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/price_encoder" + "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" + daemontypes "github.com/tellor-io/layer/daemons/types" + "github.com/tellor-io/layer/lib/metrics" + + "github.com/cometbft/cometbft/libs/log" + handler "github.com/tellor-io/layer/daemons/pricefeed/client/queryhandler" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + servertypes "github.com/tellor-io/layer/daemons/server/types" +) + +var ( + HttpClient = http.Client{ + Transport: &http.Transport{MaxConnsPerHost: constants.MaxConnectionsPerExchange}, + } +) + +// SubTaskRunnerImpl is the struct that implements the `SubTaskRunner` interface. +type SubTaskRunnerImpl struct{} + +// Ensure the `SubTaskRunnerImpl` struct is implemented at compile time. +var _ SubTaskRunner = (*SubTaskRunnerImpl)(nil) + +// SubTaskRunner is the interface for running pricefeed client task functions. +type SubTaskRunner interface { + StartPriceUpdater( + c *Client, + ctx context.Context, + ticker *time.Ticker, + stop <-chan bool, + exchangeToMarketPrices types.ExchangeToMarketPrices, + priceFeedServiceClient servertypes.PriceFeedServiceClient, + logger log.Logger, + ) + StartPriceEncoder( + exchangeId types.ExchangeId, + configs types.PricefeedMutableMarketConfigs, + exchangeToMarketPrices types.ExchangeToMarketPrices, + logger log.Logger, + bCh <-chan *price_fetcher.PriceFetcherSubtaskResponse, + ) + StartPriceFetcher( + ticker *time.Ticker, + stop <-chan bool, + configs types.PricefeedMutableMarketConfigs, + exchangeQueryConfig types.ExchangeQueryConfig, + exchangeDetails types.ExchangeQueryDetails, + queryHandler handler.ExchangeQueryHandler, + logger log.Logger, + bCh chan<- *price_fetcher.PriceFetcherSubtaskResponse, + ) +} + +// StartPriceEncoder continuously reads from a buffered channel, reading encoded API responses for exchange +// requests and inserting them into an `ExchangeToMarketPrices` cache, performing currency conversions based +// on the index price of other markets as necessary. +// StartPriceEncoder reads price fetcher responses from a shared channel, and does not need a ticker or stop +// signal from the daemon to exit. It marks itself as done in the daemon's wait group when the price fetcher +// closes the shared channel. +func (s *SubTaskRunnerImpl) StartPriceEncoder( + exchangeId types.ExchangeId, + configs types.PricefeedMutableMarketConfigs, + exchangeToMarketPrices types.ExchangeToMarketPrices, + logger log.Logger, + bCh <-chan *price_fetcher.PriceFetcherSubtaskResponse, +) { + exchangeMarketConfig, err := configs.GetExchangeMarketConfigCopy(exchangeId) + if err != nil { + panic(err) + } + + marketConfigs, err := configs.GetMarketConfigCopies(exchangeMarketConfig.GetMarketIds()) + if err != nil { + panic(err) + } + + priceEncoder, err := price_encoder.NewPriceEncoder( + exchangeMarketConfig, + marketConfigs, + exchangeToMarketPrices, + logger, + bCh, + ) + + if err != nil { + panic(err) + } + + configs.AddPriceEncoder(priceEncoder) + + // Listen for prices from the buffered channel and update the exchangeToMarketPrices cache. + // Also log any errors that occur. + for response := range bCh { + priceEncoder.ProcessPriceFetcherResponse(response) + } +} + +// StartPriceFetcher periodically starts goroutines to "fetch" market prices from a specific exchange. Each +// goroutine does the following: +// 1) query a single market price from a specific exchange +// 2) transform response to `MarketPriceTimestamp` +// 3) send transformed response to a buffered channel that's shared across multiple goroutines +// NOTE: the subtask response shared channel has a buffer size and goroutines will block if the buffer is full. +// NOTE: the price fetcher kicks off 1 to n go routines every time the subtask loop runs, but the subtask +// loop blocks until all go routines are done. This means that these go routines are not tracked by the wait group. +func (s *SubTaskRunnerImpl) StartPriceFetcher( + ticker *time.Ticker, + stop <-chan bool, + configs types.PricefeedMutableMarketConfigs, + exchangeQueryConfig types.ExchangeQueryConfig, + exchangeDetails types.ExchangeQueryDetails, + queryHandler handler.ExchangeQueryHandler, + logger log.Logger, + bCh chan<- *price_fetcher.PriceFetcherSubtaskResponse, +) { + exchangeMarketConfig, err := configs.GetExchangeMarketConfigCopy(exchangeQueryConfig.ExchangeId) + if err != nil { + panic(err) + } + + marketConfigs, err := configs.GetMarketConfigCopies(exchangeMarketConfig.GetMarketIds()) + if err != nil { + panic(err) + } + + // Create PriceFetcher to begin querying with. + priceFetcher, err := price_fetcher.NewPriceFetcher( + exchangeQueryConfig, + exchangeDetails, + exchangeMarketConfig, + marketConfigs, + queryHandler, + logger, + bCh, + ) + if err != nil { + panic(err) + } + + // The PricefeedMutableMarketConfigs object that stores the configs for each exchange + // is not initialized with the price fetcher, because both objects have references to + // each other defined during normal daemon operation. Instead, the price fetcher is + // initialized with the configs object after the price fetcher is created, and then adds + // itself to the config's list of exchange config updaters here. + configs.AddPriceFetcher(priceFetcher) + + requestHandler := daemontypes.NewRequestHandlerImpl( + &HttpClient, + ) + // Begin loop to periodically start goroutines to query market prices. + for { + select { + case <-ticker.C: + // Start goroutines to query exchange markets. The goroutines started by the price + // fetcher are not tracked by the global wait group, because RunTaskLoop will + // block until all goroutines are done. + priceFetcher.RunTaskLoop(requestHandler) + + case <-stop: + // Signal to the encoder that the price fetcher is done. + close(bCh) + return + } + } +} + +// StartPriceUpdater periodically runs a task loop to send price updates to the pricefeed server +// via: +// 1) Get `MarketPriceTimestamps` for all exchanges in an `ExchangeToMarketPrices` struct. +// 2) Transform `MarketPriceTimestamps` and exchange ids into an `UpdateMarketPricesRequest` struct. +// StartPriceUpdater runs in the daemon's main goroutine and does not need access to the daemon's wait group +// to signal task completion. +func (s *SubTaskRunnerImpl) StartPriceUpdater( + c *Client, + ctx context.Context, + ticker *time.Ticker, + stop <-chan bool, + exchangeToMarketPrices types.ExchangeToMarketPrices, + priceFeedServiceClient servertypes.PriceFeedServiceClient, + logger log.Logger, +) { + for { + select { + case <-ticker.C: + err := RunPriceUpdaterTaskLoop(ctx, exchangeToMarketPrices, priceFeedServiceClient, logger) + + if err == nil { + // Record update success for the daemon health check. + c.ReportSuccess() + } else { + logger.Error("Failed to run price updater task loop for price daemon", constants.ErrorLogKey, err) + // Record update failure for the daemon health check. + c.ReportFailure(fmt.Errorf("failed to run price updater task loop for price daemon; %v", err)) + } + + case <-stop: + return + } + } +} + +// -------------------- Task Loops -------------------- // + +// RunPriceUpdaterTaskLoop copies the map of current `exchangeId -> MarketPriceTimestamp`, +// transforms the map values into a market price update request and sends the request to the socket +// where the pricefeed server is listening. +func RunPriceUpdaterTaskLoop( + ctx context.Context, + exchangeToMarketPrices types.ExchangeToMarketPrices, + priceFeedServiceClient servertypes.PriceFeedServiceClient, + logger log.Logger, +) error { + logger = logger.With(constants.SubmoduleLogKey, constants.PriceUpdaterSubmoduleName) + priceUpdates := exchangeToMarketPrices.GetAllPrices() + request := transformPriceUpdates(priceUpdates) + + // Measure latency to send prices over gRPC. + // Note: intentionally skipping latency for `GetAllPrices`. + defer telemetry.ModuleMeasureSince( + metrics.PricefeedDaemon, + time.Now(), + metrics.PriceUpdaterSendPrices, + metrics.Latency, + ) + + // On startup the length of request will likely be 0. Even so, we return an error here because this + // is unexpected behavior once the daemon reaches a steady state. The daemon health check process should + // be robust enough to ignore temporarily unhealthy daemons. + // Sending a request of length 0, however, causes a panic. + // panic: rpc error: code = Unknown desc = Market price update has length of 0. + if len(request.MarketPriceUpdates) > 0 { + _, err := priceFeedServiceClient.UpdateMarketPrices(ctx, request) + if err != nil { + // Log error if an error will be returned from the task loop and measure failure. + logger.Error("Failed to run price updater task loop for price daemon", "error", err) + telemetry.IncrCounter( + 1, + metrics.PricefeedDaemon, + metrics.PriceUpdaterTaskLoop, + metrics.Error, + ) + return err + } + } else { + // This is expected to happen on startup until prices have been encoded into the in-memory + // `exchangeToMarketPrices` map. After that point, there should be no price updates of length 0. + logger.Info("Price update had length of 0") + telemetry.IncrCounter( + 1, + metrics.PricefeedDaemon, + metrics.PriceUpdaterZeroPrices, + metrics.Count, + ) + return fmt.Errorf("ErrEmptyMarketPriceUpdate") + } + + return nil +} + +// transformPriceUpdates transforms a map (key: exchangeId, value: list of market prices) into a +// market price update request. +func transformPriceUpdates( + updates map[types.ExchangeId][]types.MarketPriceTimestamp, +) *servertypes.UpdateMarketPricesRequest { + // Measure latency to transform prices being sent over gRPC. + defer telemetry.ModuleMeasureSince( + metrics.PricefeedDaemon, + time.Now(), + metrics.PriceUpdaterTransformPrices, + metrics.Latency, + ) + + marketPriceUpdateMap := make(map[types.MarketId]*servertypes.MarketPriceUpdate) + + // Invert to marketId -> `api.MarketPriceUpdate`. + for exchangeId, marketPriceTimestamps := range updates { + for _, marketPriceTimestamp := range marketPriceTimestamps { + marketPriceUpdate, exists := marketPriceUpdateMap[marketPriceTimestamp.MarketId] + // Add key with empty `api.MarketPriceUpdate` if entry does not exist. + if !exists { + marketPriceUpdate = &servertypes.MarketPriceUpdate{ + MarketId: marketPriceTimestamp.MarketId, + ExchangePrices: []*servertypes.ExchangePrice{}, + } + marketPriceUpdateMap[marketPriceTimestamp.MarketId] = marketPriceUpdate + } + + // Add `api.ExchangePrice`. + priceUpdateTime := marketPriceTimestamp.LastUpdatedAt + exchangePrice := &servertypes.ExchangePrice{ + ExchangeId: exchangeId, + Price: marketPriceTimestamp.Price, + LastUpdateTime: &priceUpdateTime, + } + marketPriceUpdate.ExchangePrices = append(marketPriceUpdate.ExchangePrices, exchangePrice) + } + } + + // Add all `api.MarketPriceUpdate` to request to be sent by `client.UpdateMarketPrices`. + request := &servertypes.UpdateMarketPricesRequest{ + MarketPriceUpdates: make([]*servertypes.MarketPriceUpdate, 0, len(marketPriceUpdateMap)), + } + for _, update := range marketPriceUpdateMap { + request.MarketPriceUpdates = append( + request.MarketPriceUpdates, + update, + ) + } + return request +} diff --git a/daemons/pricefeed/client/types/exchange_config_json.go b/daemons/pricefeed/client/types/exchange_config_json.go new file mode 100644 index 000000000..2abfe39d5 --- /dev/null +++ b/daemons/pricefeed/client/types/exchange_config_json.go @@ -0,0 +1,36 @@ +package types + +import ( + "fmt" +) + +// ExchangeConfigJson demarshals the exchange configuration json for a particular market. +// The result is a list of parameters that define how the market is resolved on +// each supported exchange. +// +// This struct stores data in an intermediate form as it's being assigned to various +// `ExchangeMarketConfig` objects, which are keyed by exchange id. These objects are not kept +// past the time the `GetAllMarketParams` API response is parsed, and do not contain an id +// because the id is expected to be known at the time the object is in use. +type ExchangeConfigJson struct { + Exchanges []ExchangeMarketConfigJson `json:"exchanges"` +} + +// Validate validates the exchange configuration json, checking that required fields are defined +// and that market and exchange names correspond to valid markets and exchanges. +func (ecj *ExchangeConfigJson) Validate( + exchangeNames []ExchangeId, + marketNames map[string]MarketId, +) error { + if len(ecj.Exchanges) == 0 { + return fmt.Errorf("exchanges cannot be empty") + } + + for _, exchange := range ecj.Exchanges { + err := exchange.Validate(exchangeNames, marketNames) + if err != nil { + return fmt.Errorf("invalid exchange: %w", err) + } + } + return nil +} diff --git a/daemons/pricefeed/client/types/exchange_id.go b/daemons/pricefeed/client/types/exchange_id.go new file mode 100644 index 000000000..bd4573aa4 --- /dev/null +++ b/daemons/pricefeed/client/types/exchange_id.go @@ -0,0 +1,4 @@ +package types + +// The id will be matched against each exchange's `exchangeName` in the `MarketParam`'s `exchange_config_json`. +type ExchangeId = string diff --git a/daemons/pricefeed/client/types/exchange_market_config_json.go b/daemons/pricefeed/client/types/exchange_market_config_json.go new file mode 100644 index 000000000..2bfc8e51a --- /dev/null +++ b/daemons/pricefeed/client/types/exchange_market_config_json.go @@ -0,0 +1,45 @@ +package types + +import ( + "fmt" +) + +// ExchangeMarketConfigJson captures per-exchange information for resolving a market, including +// the ticker and conversion details. It demarshals JSON parameters from the chain for a +// particular market on a specific exchange. +type ExchangeMarketConfigJson struct { + ExchangeName string `json:"exchangeName"` + Ticker string `json:"ticker"` + AdjustByMarket string `json:"adjustByMarket,omitempty"` + Invert bool `json:"invert,omitempty"` +} + +// Validate validates the exchange market configuration json. It returns an error if the +// configuration is invalid. +func (emcj *ExchangeMarketConfigJson) Validate( + exchangeIds []ExchangeId, + marketNames map[string]MarketId, +) error { + // Build a map with exchange names as keys for ease of membership testing. The exchange names + // in the config should match the exchange ids exactly. + exchangeNameMap := make(map[ExchangeId]struct{}) + for _, exchangeName := range exchangeIds { + exchangeNameMap[exchangeName] = struct{}{} + } + + if emcj.ExchangeName == "" { + return fmt.Errorf("exchange name cannot be empty") + } + if _, exists := exchangeNameMap[emcj.ExchangeName]; !exists { + return fmt.Errorf("exchange name '%v' is not valid", emcj.ExchangeName) + } + if emcj.Ticker == "" { + return fmt.Errorf("ticker cannot be empty") + } + if emcj.AdjustByMarket != "" { + if _, exists := marketNames[emcj.AdjustByMarket]; !exists { + return fmt.Errorf("adjustment market '%v' is not valid", emcj.AdjustByMarket) + } + } + return nil +} diff --git a/daemons/pricefeed/client/types/exchange_query_config.go b/daemons/pricefeed/client/types/exchange_query_config.go new file mode 100644 index 000000000..30a3a9f8c --- /dev/null +++ b/daemons/pricefeed/client/types/exchange_query_config.go @@ -0,0 +1,20 @@ +package types + +// ExchangeQueryConfig contains configuration values for querying an exchange, passed in on startup. +// The configuration values include +// 1. `ExchangeId` +// 2. `IntervalMs` delay between task-loops where each task-loop sends API requests to an exchange +// 3. `TimeoutMs` max time to wait on an API call to an exchange +// 4. `MaxQueries` max number of API calls made to an exchange per task-loop. This parameter is used +// for rate limiting requests to the exchange. +// +// For single-market API exchanges, the price fetcher will send approximately +// MaxQueries API responses into the exchange's buffered channel once every IntervalMs milliseconds. +// Note: the `ExchangeQueryConfig` will be used in the map of `{ exchangeId, `ExchangeQueryConfig` }` +// that dictates how the pricefeed client queries for market prices. +type ExchangeQueryConfig struct { + ExchangeId ExchangeId `json:"exchange_id"` + IntervalMs uint32 `json:"interval_ms"` + TimeoutMs uint32 `json:"timeout_ms"` + MaxQueries uint32 `json:"max_queries"` +} diff --git a/daemons/pricefeed/client/types/exchange_query_details.go b/daemons/pricefeed/client/types/exchange_query_details.go new file mode 100644 index 000000000..8bf3d0861 --- /dev/null +++ b/daemons/pricefeed/client/types/exchange_query_details.go @@ -0,0 +1,26 @@ +package types + +import ( + "net/http" + + pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" +) + +// ExchangeQueryDetails represents the information needed to query a specific exchange. +type ExchangeQueryDetails struct { + Exchange ExchangeId + // Url is the url to query the exchange. + Url string + // PriceFunction computes a map of tickers to prices from an exchange's response + PriceFunction func( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pricefeedtypes.Resolver, + ) ( + tickerToPrice map[string]uint64, + unavailableTickers map[string]error, + err error, + ) + // IsMultiMarket indicates whether the url query response contains multiple tickers. + IsMultiMarket bool +} diff --git a/daemons/pricefeed/client/types/exchange_to_market_prices.go b/daemons/pricefeed/client/types/exchange_to_market_prices.go new file mode 100644 index 000000000..7add29043 --- /dev/null +++ b/daemons/pricefeed/client/types/exchange_to_market_prices.go @@ -0,0 +1,137 @@ +package types + +import ( + "errors" + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/lib/metrics" +) + +// ExchangeToMarketPrices maintains price info for multiple exchanges. Each exchange can support +// prices from multiple market sources. Methods are goroutine safe in the underlying MarketToPrice +// objects. +type ExchangeToMarketPrices interface { + UpdatePrice( + exchangeId ExchangeId, + marketPriceTimestamp *MarketPriceTimestamp, + ) + GetAllPrices() map[ExchangeId][]MarketPriceTimestamp + GetIndexPrice( + marketId MarketId, + cutoffTime time.Time, + resolver types.Resolver, + ) ( + medianPrice uint64, + numPricesMedianized int, + ) +} + +type ExchangeToMarketPricesImpl struct { + // {k: exchangeId, v: market prices, read-write lock} + ExchangeMarketPrices map[ExchangeId]*MarketToPrice +} + +// Enforce conformity of ExchangeToMarketPricesImpl to ExchangeToMarketPrices interface at compile time. +var _ ExchangeToMarketPrices = &ExchangeToMarketPricesImpl{} + +// NewExchangeToMarketPrices creates a new ExchangeToMarketPrices. Since `ExchangeToMarketPrices` is +// not goroutine safe to write to, all key-value store creation is done on initialization. +// Validation is also done to verify `exchangeIds` is a valid input. +func NewExchangeToMarketPrices(exchangeIds []ExchangeId) (ExchangeToMarketPrices, error) { + // Verify `ExchangeToMarketPrices` will not be initialized without `exchangeIds`. + if len(exchangeIds) == 0 { + return nil, errors.New("exchangeIds must not be empty") + } + + exchangeToMarketPrices := &ExchangeToMarketPricesImpl{ + ExchangeMarketPrices: make(map[ExchangeId]*MarketToPrice, len(exchangeIds)), + } + + for _, exchangeId := range exchangeIds { + // Verify no `exchangeIds` are duplicates. + if _, ok := exchangeToMarketPrices.ExchangeMarketPrices[exchangeId]; ok { + return nil, fmt.Errorf("exchangeId: '%v' appears twice in request", exchangeId) + } + + exchangeToMarketPrices.ExchangeMarketPrices[exchangeId] = NewMarketToPrice() + } + + return exchangeToMarketPrices, nil +} + +// UpdatePrice updates a price for a market for an exchange. Prices are only updated if the +// timestamp on the updates are greater than the timestamp on existing prices. NOTE: +// `UpdatePrice` will only ever read from `ExchangeMarketPrices` and calls a +// goroutine-safe method on the fetched `MarketToPrice`. +// Note: if an invalid `exchangeId` is being written to the `UpdatePrice` it is possible the +// underlying map was corrupted or the priceDaemon logic is invalid. Therefore, `UpdatePrice` +// will panic. +func (exchangeToMarketPrices *ExchangeToMarketPricesImpl) UpdatePrice( + exchangeId ExchangeId, + marketPriceTimestamp *MarketPriceTimestamp, +) { + // Measure latency to update price in in-memory map. + defer telemetry.ModuleMeasureSince( + metrics.PricefeedDaemon, + time.Now(), + metrics.PriceEncoderUpdatePrice, + metrics.Latency, + ) + + exchangeToMarketPrices.ExchangeMarketPrices[exchangeId].UpdatePrice(marketPriceTimestamp) +} + +// GetAllPrices returns a map of exchangeIds to a list of all `MarketPriceTimestamps` for the exchange. +func (exchangeToMarketPrices *ExchangeToMarketPricesImpl) GetAllPrices() map[ExchangeId][]MarketPriceTimestamp { + // Measure latency to get all prices from in-memory map. + defer telemetry.ModuleMeasureSince( + metrics.PricefeedDaemon, + time.Now(), + metrics.GetAllPrices_MarketIdToPrice, + metrics.Latency, + ) + + exchangeIdToPrices := make( + map[ExchangeId][]MarketPriceTimestamp, + len(exchangeToMarketPrices.ExchangeMarketPrices), + ) + + for exchangeId, mtp := range exchangeToMarketPrices.ExchangeMarketPrices { + marketPrices := mtp.GetAllPrices() + exchangeIdToPrices[exchangeId] = marketPrices + } + + return exchangeIdToPrices +} + +// GetIndexPrice returns the index price for a given marketId, disallowing prices that are older than cutoffTime. +// If no valid prices are found, an error is returned. +func (exchangeToMarketPrices *ExchangeToMarketPricesImpl) GetIndexPrice( + marketId MarketId, + cutoffTime time.Time, + resolver types.Resolver, +) ( + medianPrice uint64, + numPricesMedianized int, +) { + prices := make([]uint64, 0, len(exchangeToMarketPrices.ExchangeMarketPrices)) + for _, mtp := range exchangeToMarketPrices.ExchangeMarketPrices { + price, ok := mtp.GetValidPriceForMarket(marketId, cutoffTime) + if ok { + prices = append(prices, price) + } + } + + if len(prices) == 0 { + return 0, 0 + } + median, err := resolver(prices) + + if err != nil { + return 0, 0 + } + return median, len(prices) +} diff --git a/daemons/pricefeed/client/types/exponent.go b/daemons/pricefeed/client/types/exponent.go new file mode 100644 index 000000000..acfdae31f --- /dev/null +++ b/daemons/pricefeed/client/types/exponent.go @@ -0,0 +1,4 @@ +package types + +// Exponent denotes the number of decimals a value should be shifted. +type Exponent = int32 diff --git a/daemons/pricefeed/client/types/market_config.go b/daemons/pricefeed/client/types/market_config.go new file mode 100644 index 000000000..14ee4d80b --- /dev/null +++ b/daemons/pricefeed/client/types/market_config.go @@ -0,0 +1,65 @@ +package types + +// MarketConfig specifies the exchange-specific market configuration used to resolve a market's price on +// a particular exchange. +type MarketConfig struct { + // Ticker specifies the string to use to query the relevant ticker price for a market on this exchange. + Ticker string + + // AdjustByMarket optionally identifies the appropriate market that should be used to adjust + // the price of the market ticker to arrive at the final USD price of the market. This is used in + // cases where we choose to query a price for a market on a particular exchange in a different quote + // currency - perhaps because that market is more robust - and convert the price back to the quote + // currency of the original market with another market's price. + // + // For example, for resolving the BTC-USD price on this exchange, we may use a "BTC-USDT" ticker with + // an adjust-by market of USDT-USD, and compute BTC-USD as + // + // BTC-USD = BTC-USDT * USDT-USD + // + // If this field is nil, then the market has no adjust-by market. + AdjustByMarket *MarketId + + // Invert specifies the inversion strategy to use when converting the price of the market's ticker to arrive + // at the final USD price of the market. The application of the inversion strategy depends on whether an adjust-by + // market is defined for this market. + // + // If an adjust-by market is defined for this market, then the inversion strategy is applied with respect + // to the adjustment market. For example, say we use a "BTC-USDT" ticker for USDT-USD on this exchange, with + // an adjust-by market of BTC-USD, and an inversion value of true. In this case, we are describing that + // we will derive the BTC-USD price by multiplying the BTC-USD index price by the inverse of the BTC-USDT ticker + // price: + // + // USDT-USD = BTC-USD / BTC-USDT + // + // If an adjust-by market is not defined for this market, then the inversion strategy is applied to the ticker + // price itself. For example, for BTC, say we use "USD-BTC" as the BTC-USD ticker on this exchange with an + // inversion value of true. In that case, we would derive the BTC-USD price by taking the inverse of the + // USD-BTC price: + // + // BTC-USD = 1 / USD-BTC + Invert bool +} + +// Equal returns true if the two MarketConfigs are equal. +func (mc *MarketConfig) Equal(other MarketConfig) bool { + return mc.Ticker == other.Ticker && + mc.Invert == other.Invert && + ((mc.AdjustByMarket == nil && other.AdjustByMarket == nil) || + (mc.AdjustByMarket != nil && other.AdjustByMarket != nil && + *mc.AdjustByMarket == *other.AdjustByMarket)) +} + +// Copy returns a deep copy of the MarketConfig. +func (mc *MarketConfig) Copy() MarketConfig { + var adjustByMarket *MarketId + if mc.AdjustByMarket != nil { + adjustByMarket = new(MarketId) + *adjustByMarket = *mc.AdjustByMarket + } + return MarketConfig{ + Ticker: mc.Ticker, + AdjustByMarket: adjustByMarket, + Invert: mc.Invert, + } +} diff --git a/daemons/pricefeed/client/types/market_id.go b/daemons/pricefeed/client/types/market_id.go new file mode 100644 index 000000000..92015d4de --- /dev/null +++ b/daemons/pricefeed/client/types/market_id.go @@ -0,0 +1,4 @@ +package types + +// MarketId is the unique id for a `Market`. +type MarketId = uint32 diff --git a/daemons/pricefeed/client/types/market_param.go b/daemons/pricefeed/client/types/market_param.go new file mode 100644 index 000000000..48d2bb027 --- /dev/null +++ b/daemons/pricefeed/client/types/market_param.go @@ -0,0 +1,70 @@ +package types + +import ( + "encoding/json" + "fmt" +) + +const ( + MaxPriceChangePpm = uint32(10_000) +) + +type MarketParam struct { + // Unique, sequentially-generated value. + Id uint32 + // The human-readable name of the market pair (e.g. `BTC-USD`). + Pair string + // Static value. The exponent of the price. + // For example if `Exponent == -5` then a `Value` of `1,000,000,000` + // represents “$10,000`. Therefore `10 ^ Exponent` represents the smallest + // price step (in dollars) that can be recorded. + Exponent int32 + // The minimum number of exchanges that should be reporting a live price for + // a price update to be considered valid. + MinExchanges uint32 + // The minimum allowable change in `price` value that would cause a price + // update on the network. Measured as `1e-6` (parts per million). + MinPriceChangePpm uint32 + // A string of json that encodes the configuration for resolving the price + // of this market on various exchanges. + ExchangeConfigJson string +} + +// Validate checks that the MarketParam is valid. +func (mp *MarketParam) Validate() error { + // Validate pair. + if mp.Pair == "" { + return fmt.Errorf("Invalid input: Pair cannot be empty") + } + + if mp.MinExchanges == 0 { + return fmt.Errorf("Min exchanges must be greater than zero") + } + + // Validate min price change. + if mp.MinPriceChangePpm == 0 || mp.MinPriceChangePpm >= MaxPriceChangePpm { + return fmt.Errorf( + "Invalid input Min price change in parts-per-million must be greater than 0 and less than %d", + MaxPriceChangePpm) + } + + if err := IsValidJSON(mp.ExchangeConfigJson); err != nil { + return fmt.Errorf( + "Invalid input: ExchangeConfigJson string is not valid: err=%v, input=%v", + err, + mp.ExchangeConfigJson, + ) + } + + return nil +} + +// IsValidJSON checks if a JSON string is well-formed. +func IsValidJSON(str string) error { + var js map[string]interface{} + err := json.Unmarshal([]byte(str), &js) + if err != nil { + return err + } + return nil +} diff --git a/daemons/pricefeed/client/types/market_price_timestamp.go b/daemons/pricefeed/client/types/market_price_timestamp.go new file mode 100644 index 000000000..33f8cdbcc --- /dev/null +++ b/daemons/pricefeed/client/types/market_price_timestamp.go @@ -0,0 +1,10 @@ +package types + +import "time" + +// MarketPriceTimestamp maintains a `MarketId`, `Price` and `LastUpdatedAt`. +type MarketPriceTimestamp struct { + MarketId uint32 + Price uint64 + LastUpdatedAt time.Time +} diff --git a/daemons/pricefeed/client/types/market_to_price.go b/daemons/pricefeed/client/types/market_to_price.go new file mode 100644 index 000000000..f9eb74692 --- /dev/null +++ b/daemons/pricefeed/client/types/market_to_price.go @@ -0,0 +1,81 @@ +package types + +import ( + "sync" + "time" + + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/lib/metrics" +) + +// MarketToPrice maintains multiple prices for different markets for the same exchange, +// along with the last time that each market price was updated. +// Methods are goroutine safe. +type MarketToPrice struct { + sync.Mutex // lock + MarketToPriceTimestamp map[uint32]*types.PriceTimestamp // {k: market id, v: PriceTimestamp} +} + +// NewMarketToPrice creates a new MarketToPrice. +func NewMarketToPrice() *MarketToPrice { + return &MarketToPrice{ + MarketToPriceTimestamp: make(map[uint32]*types.PriceTimestamp), + } +} + +// UpdatePrice updates a price for a market for an exchange. +// Prices are only updated if the timestamp on the updates are greater than +// the timestamp on existing prices. +func (mtp *MarketToPrice) UpdatePrice( + marketPriceTimestamp *MarketPriceTimestamp, +) { + mtp.Lock() + defer mtp.Unlock() + + marketId := marketPriceTimestamp.MarketId + priceTimestamp, ok := mtp.MarketToPriceTimestamp[marketId] + if !ok { + priceTimestamp = types.NewPriceTimestamp() + mtp.MarketToPriceTimestamp[marketId] = priceTimestamp + } + isUpdated := priceTimestamp.UpdatePrice(marketPriceTimestamp.Price, &marketPriceTimestamp.LastUpdatedAt) + + validity := metrics.Valid + if !isUpdated { + validity = metrics.PriceIsInvalid + } + + // Measure count of valid and invalid prices inserted into the in-memory map. + telemetry.IncrCounter(1, metrics.PricefeedDaemon, metrics.UpdatePrice, validity, metrics.Count) +} + +// GetAllPrices returns a list of all `MarketPriceTimestamps` for an exchange. +func (mtp *MarketToPrice) GetAllPrices() []MarketPriceTimestamp { + mtp.Lock() + defer mtp.Unlock() + + marketPricesForExchange := make([]MarketPriceTimestamp, 0, len(mtp.MarketToPriceTimestamp)) + for marketId, priceTimestamp := range mtp.MarketToPriceTimestamp { + mpt := MarketPriceTimestamp{ + MarketId: marketId, + LastUpdatedAt: priceTimestamp.LastUpdateTime, + Price: priceTimestamp.Price, + } + marketPricesForExchange = append(marketPricesForExchange, mpt) + } + + return marketPricesForExchange +} + +// GetValidPriceForMarket returns the most recent valid price for a market for an exchange. +func (mtp *MarketToPrice) GetValidPriceForMarket(marketId MarketId, cutoffTime time.Time) (uint64, bool) { + mtp.Lock() + defer mtp.Unlock() + price, exists := mtp.MarketToPriceTimestamp[marketId] + if !exists { + return 0, false + } + + return price.GetValidPrice(cutoffTime) +} diff --git a/daemons/pricefeed/client/types/mutable_exchange_config_updater.go b/daemons/pricefeed/client/types/mutable_exchange_config_updater.go new file mode 100644 index 000000000..d1e8174f9 --- /dev/null +++ b/daemons/pricefeed/client/types/mutable_exchange_config_updater.go @@ -0,0 +1,17 @@ +package types + +// ExchangeConfigUpdater is the interface that wraps the UpdateMutableExchangeConfig method. +// ExchangeConfigUpdater objects are keyed by exchange id and receive updates notifying +// them that the mutable exchange market configuration has been updated, along with the all new configs. +// This interface is added to avoid import loops that occur when importing the `PriceFetcher` type +// directly into `PricefeedMutableMarketConfigs`. +type ExchangeConfigUpdater interface { + GetExchangeId() ExchangeId + // UpdateMutableExchangeConfig notifies the object that the mutable exchange market configuration + // for this object's exchange has been updated with a new configuration. It also provides + // the current market configs for all supported markets on the exchange. + UpdateMutableExchangeConfig( + newExchangeConfig *MutableExchangeMarketConfig, + newMarketConfigs []*MutableMarketConfig, + ) error +} diff --git a/daemons/pricefeed/client/types/mutable_exchange_market_config.go b/daemons/pricefeed/client/types/mutable_exchange_market_config.go new file mode 100644 index 000000000..d37cb8d59 --- /dev/null +++ b/daemons/pricefeed/client/types/mutable_exchange_market_config.go @@ -0,0 +1,84 @@ +package types + +import ( + "fmt" + + "github.com/tellor-io/layer/lib" +) + +// MutableExchangeMarketConfig stores all mutable market configuration per exchange. +type MutableExchangeMarketConfig struct { + Id ExchangeId + // We use the keys of MarketToMarketConfig to infer which markets are supported + // by the exchange. + MarketToMarketConfig map[MarketId]MarketConfig +} + +// Copy returns a copy of the MutableExchangeMarketConfig. +func (memc *MutableExchangeMarketConfig) Copy() *MutableExchangeMarketConfig { + marketToMarketConfig := make(map[MarketId]MarketConfig, len(memc.MarketToMarketConfig)) + for market, config := range memc.MarketToMarketConfig { + marketToMarketConfig[market] = config.Copy() + } + return &MutableExchangeMarketConfig{ + Id: memc.Id, + MarketToMarketConfig: marketToMarketConfig, + } +} + +// GetMarketIds returns the ordered list of market ids supported by the exchange. This set is +// currently implicitly defined by the keys of the MarketToTicker map. +func (memc *MutableExchangeMarketConfig) GetMarketIds() []MarketId { + return lib.GetSortedKeys[lib.Sortable[uint32]](memc.MarketToMarketConfig) +} + +func (memc *MutableExchangeMarketConfig) Validate(marketConfigs []*MutableMarketConfig) error { + marketToMutableConfig := make(map[MarketId]*MutableMarketConfig, len(marketConfigs)) + for _, mutableMarketConfig := range marketConfigs { + marketToMutableConfig[mutableMarketConfig.Id] = mutableMarketConfig + } + + for marketId, config := range memc.MarketToMarketConfig { + mutableMarketConfig, exists := marketToMutableConfig[marketId] + if !exists { + return fmt.Errorf("no market config for market %v on exchange '%v'", marketId, memc.Id) + } + if err := mutableMarketConfig.Validate(); err != nil { + return fmt.Errorf("invalid market config for market %v on exchange '%v': %w", marketId, memc.Id, err) + } + + if config.AdjustByMarket != nil { + if _, exists := marketToMutableConfig[*config.AdjustByMarket]; !exists { + return fmt.Errorf( + "no market config for adjust-by market %v used to convert market %v price on exchange '%v'", + *config.AdjustByMarket, + marketId, + memc.Id, + ) + } + } + } + return nil +} + +// Equal returns true if the two MutableExchangeMarketConfig objects are equal. +func (memc *MutableExchangeMarketConfig) Equal(other *MutableExchangeMarketConfig) bool { + if memc.Id != other.Id { + return false + } + if len(memc.MarketToMarketConfig) != len(other.MarketToMarketConfig) { + return false + } + + for market, config := range memc.MarketToMarketConfig { + otherConfig, exists := other.MarketToMarketConfig[market] + if !exists { + return false + } + if !config.Equal(otherConfig) { + return false + } + } + + return true +} diff --git a/daemons/pricefeed/client/types/mutable_market_config.go b/daemons/pricefeed/client/types/mutable_market_config.go new file mode 100644 index 000000000..07fe289bd --- /dev/null +++ b/daemons/pricefeed/client/types/mutable_market_config.go @@ -0,0 +1,32 @@ +package types + +import "fmt" + +// MutableMarketConfig stores the metadata that is common to a market across exchanges. +type MutableMarketConfig struct { + Id MarketId + Pair string + Exponent Exponent + MinExchanges uint32 +} + +// Copy returns a copy of the MutableMarketConfig. +func (mmc *MutableMarketConfig) Copy() *MutableMarketConfig { + return &MutableMarketConfig{ + Id: mmc.Id, + Pair: mmc.Pair, + Exponent: mmc.Exponent, + MinExchanges: mmc.MinExchanges, + } +} + +func (mmc *MutableMarketConfig) Validate() error { + if mmc.Pair == "" { + return fmt.Errorf("pair cannot be empty") + } + if mmc.MinExchanges == 0 { + return fmt.Errorf("min exchanges cannot be 0") + } + + return nil +} diff --git a/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go b/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go new file mode 100644 index 000000000..11254480f --- /dev/null +++ b/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go @@ -0,0 +1,583 @@ +package types + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + + gometrics "github.com/armon/go-metrics" + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/lib/metrics" +) + +const ( + expectedUpdatersPerExchange = 2 +) + +// PricefeedMutableMarketConfigs stores a single copy of all market state that can change dynamically and synchronizes +// access for running go routines within the daemon. +type PricefeedMutableMarketConfigs interface { + AddPriceFetcher(updater ExchangeConfigUpdater) + AddPriceEncoder(updater ExchangeConfigUpdater) + UpdateMarkets(marketParams []MarketParam) (marketParamErrors map[MarketId]error, err error) + GetExchangeMarketConfigCopy( + id ExchangeId, + ) ( + mutableExchangeMarketConfig *MutableExchangeMarketConfig, + err error, + ) + GetMarketConfigCopies( + markets []MarketId, + ) ( + mutableMarketConfigs []*MutableMarketConfig, + err error, + ) +} + +// Ensure the `PricefeedMutableMarketConfigsImpl` struct is implemented at compile time. +var _ PricefeedMutableMarketConfigs = &PricefeedMutableMarketConfigsImpl{} + +// UpdatersForExchange contains named references to all ExchangeConfigUpdaters for a single exchange. +type UpdatersForExchange struct { + PriceFetcher ExchangeConfigUpdater + PriceEncoder ExchangeConfigUpdater +} + +// UpdateParameters contains the parameters to send to an ExchangeConfigUpdater when the exchange config changes. +type UpdateParameters struct { + ExchangeConfig *MutableExchangeMarketConfig + MarketConfigs []*MutableMarketConfig +} + +func (ufe *UpdatersForExchange) Validate() error { + if ufe.PriceFetcher == nil { + return fmt.Errorf("price fetcher cannot be nil") + } + if ufe.PriceEncoder == nil { + return fmt.Errorf("price encoder cannot be nil") + } + return nil +} + +// PricefeedMutableMarketConfigsImpl implements PricefeedMutableMarketConfigs. +type PricefeedMutableMarketConfigsImpl struct { + sync.Mutex + + // mutableExchangeToConfigs contains the latest market configuration for each exchange. + // These maps are updated when the exchange market params are updated. Map reads + // and updates are synchronized by the Mutex. + mutableExchangeToConfigs map[ExchangeId]*MutableExchangeMarketConfig + + // mutableMarketToConfigs contains the latest market configuration for each market, common + // across all exchanges. + mutableMarketToConfigs map[MarketId]*MutableMarketConfig + + // mutableExchangeConfigUpdaters contains a map of `ExchangeConfigUpdater`s for each exchange. + // In reality, these ExchangeConfigUpdaters are pointers to price fetchers and price encoders. + // It is initialized with no updaters, and each price fetcher and encoder subscribes to updates + // from the PricefeedMutableMarketConfigsImpl on creation. + // + // Whenever the mutable exchange market config for an exchange updates, the + // PriceFeedMutableMarketConfigsImpl calls `UpdateMutableExchangeConfig` on each subscribed fetcher + // and encoder. The encoder is called first so that it has all necessary market config to support + // price updates coming from the fetcher in the event of adding a new market. + // + // Once a key is populated, the value never changes. The map is populated when updaters are + // created at application start. Individual updaters manage their own synchronization. + mutableExchangeConfigUpdaters map[ExchangeId]UpdatersForExchange + + // updatersInitialized tracks whether all expected updaters have been added to the pricefeed mutable market configs. + // This is used to ensure that all updaters are subscribed before the pricefeed mutable market configs processes or + // emits updates. The pfmmc only emit updates to exchange config updaters when the config changes, so any missing + // subscribers would never receive an update until the next change. + updatersInitialized sync.WaitGroup +} + +// NewPriceFeedMutableMarketConfigs creates a new PricefeedMutableMarketConfigsImpl with no markets assigned +// to any exchanges. Apply market settings by calling `UpdateMarkets`. +func NewPriceFeedMutableMarketConfigs( + canonicalExchangeIds []ExchangeId, +) *PricefeedMutableMarketConfigsImpl { + exchangeIdToMutableExchangeConfigUpdater := make( + map[ExchangeId]UpdatersForExchange, + len(canonicalExchangeIds), + ) + + // Initialize the mutableExchangeConfigs for all exchanges with no markets. + mutableExchangeToConfigs := make(map[ExchangeId]*MutableExchangeMarketConfig, len(canonicalExchangeIds)) + for _, exchangeId := range canonicalExchangeIds { + mutableExchangeToConfigs[exchangeId] = &MutableExchangeMarketConfig{ + Id: exchangeId, + MarketToMarketConfig: make(map[MarketId]MarketConfig, 0), + } + } + + pfmmc := &PricefeedMutableMarketConfigsImpl{ + mutableExchangeToConfigs: mutableExchangeToConfigs, + mutableMarketToConfigs: nil, + mutableExchangeConfigUpdaters: exchangeIdToMutableExchangeConfigUpdater, + } + + // Add the expected number of registered updaters to the wait group. + pfmmc.updatersInitialized.Add(expectedUpdatersPerExchange * len(canonicalExchangeIds)) + + return pfmmc +} + +// AddPriceFetcher adds a new price fetcher to the pricefeed mutable market configs. This method is synchronized. +func (pfmmc *PricefeedMutableMarketConfigsImpl) AddPriceFetcher( + priceFetcher ExchangeConfigUpdater, +) { + pfmmc.addExchangeConfigUpdater(priceFetcher, true) +} + +// AddPriceEncoder adds a new price encoder to the pricefeed mutable market configs. This method is synchronized. +func (pfmmc *PricefeedMutableMarketConfigsImpl) AddPriceEncoder( + priceEncoder ExchangeConfigUpdater, +) { + pfmmc.addExchangeConfigUpdater(priceEncoder, false) +} + +// AddExchangeConfigUpdater adds a new exchange config updater to the pricefeed mutable market configs. +// This synchronized method is how a price fetcher or encoder subscribes itself in PricefeedMutableMarketConfigs +// for exchange configuration updates. +// +// This method was added to the pricefeed mutable market configs because fetchers and encoders are initialized +// with a pointer to the pricefeed mutable market configs, and the pricefeed mutable market configs also +// needs a reference to the updater to be fully initialized - so it was decided to initialize the +// pricefeed mutable market configs first, and then have updaters add themselves. +func (pfmmc *PricefeedMutableMarketConfigsImpl) addExchangeConfigUpdater( + updater ExchangeConfigUpdater, + isPriceFetcher bool, +) { + pfmmc.Lock() + defer pfmmc.Unlock() + + updatersForExchange, exists := pfmmc.mutableExchangeConfigUpdaters[updater.GetExchangeId()] + if !exists { + updatersForExchange = UpdatersForExchange{} + } + if isPriceFetcher { + // Enforce that each updater can be added only once. + if updatersForExchange.PriceFetcher != nil { + panic(fmt.Sprintf("internal error: price fetcher already exists for exchange %v", updater.GetExchangeId())) + } + updatersForExchange.PriceFetcher = updater + } else { + // Enforce that each updater can be added only once. + if updatersForExchange.PriceEncoder != nil { + panic(fmt.Sprintf("internal error: price encoder already exists for exchange %v", updater.GetExchangeId())) + } + updatersForExchange.PriceEncoder = updater + } + + pfmmc.mutableExchangeConfigUpdaters[updater.GetExchangeId()] = updatersForExchange + pfmmc.updatersInitialized.Done() +} + +// ValidateAndTransformParams validates the market params and transforms them into the internal representation used +// by the PricefeedMutableMarketConfigsImpl. The method guarantees that the returned mutableExchangeConfigs will have +// an entry for all current exchange feeds. This method is exposed for testing. +// MarketParams are validated and applied independently. If any market param is invalid, the method will populate +// marketParamErrors with the error and continue processing the remaining market params. If the entire validation fails, +// the method will return an error. +func (pfmmc *PricefeedMutableMarketConfigsImpl) ValidateAndTransformParams(marketParams []MarketParam) ( + mutableExchangeConfigs map[ExchangeId]*MutableExchangeMarketConfig, + mutableMarketConfigs map[MarketId]*MutableMarketConfig, + marketParamErrors map[MarketId]error, + err error, +) { + // Track individual errors for each market param that fails to apply. + marketParamErrors = make(map[MarketId]error, len(marketParams)) + + if marketParams == nil { + return nil, nil, nil, fmt.Errorf("marketParams cannot be nil") + } + + mutableMarketConfigs = make(map[MarketId]*MutableMarketConfig, len(marketParams)) + + mutableExchangeConfigs = make(map[ExchangeId]*MutableExchangeMarketConfig, len(pfmmc.mutableExchangeToConfigs)) + // Initialize mutableExchangeConfigs with empty MutableExchangeMarketConfigs to make sure that each exchange + // has an entry in the map. The set of exchanges is fixed and defined at compile time. We need + // mutableExchangeMarketConfigs to be defined for all exchanges so that we can update the respective updaters. + for exchangeId := range pfmmc.mutableExchangeToConfigs { + mutableExchangeConfigs[exchangeId] = &MutableExchangeMarketConfig{ + Id: exchangeId, + MarketToMarketConfig: map[MarketId]MarketConfig{}, + } + } + + // marketNameToId, exchangeNameToId used to validate and interpret ExchangeConfigJson values. + marketNameToId := make(map[string]MarketId, len(pfmmc.mutableMarketToConfigs)) + for _, param := range marketParams { + marketNameToId[param.Pair] = param.Id + } + + exchangeNames := make([]ExchangeId, 0, len(pfmmc.mutableExchangeToConfigs)) + for exchangeName := range pfmmc.mutableExchangeToConfigs { + exchangeNames = append(exchangeNames, exchangeName) + } + + for _, marketParam := range marketParams { + // Perform validation on the market params. + if err := marketParam.Validate(); err != nil { + marketParamErrors[marketParam.Id] = fmt.Errorf("invalid market param %v: %w", marketParam.Id, err) + continue + } + + // Check for duplicate market params. + if _, exists := mutableMarketConfigs[marketParam.Id]; exists { + // In this case, return an error, because we do not know which market param is the correct one. + return nil, + nil, + nil, + fmt.Errorf("invalid market params: duplicate market id %v", marketParam.Id) + } + + var exchangeConfigJson ExchangeConfigJson + err = json.Unmarshal([]byte(marketParam.ExchangeConfigJson), &exchangeConfigJson) + if err != nil { + wrappedErr := fmt.Errorf("invalid exchange config json for market param %v: %w", marketParam.Id, err) + marketParamErrors[marketParam.Id] = wrappedErr + continue + } + + err = exchangeConfigJson.Validate(exchangeNames, marketNameToId) + if err != nil { + marketParamErrors[marketParam.Id] = fmt.Errorf( + "invalid exchange config json for market param %v: %w", + marketParam.Id, + err, + ) + continue + } + + // Errors in the following loop are unexpected because we have already validated the exchange config json. + // In this case, we return an error. + for _, exchangeConfig := range exchangeConfigJson.Exchanges { + exchangeId := exchangeConfig.ExchangeName + mutableExchangeConfig, ok := mutableExchangeConfigs[exchangeId] + if !ok { + err := fmt.Errorf( + "unexpected internal error: exchange '%v' not found in mutableExchangeConfigs for market %v", + exchangeId, + marketParam.Pair, + ) + return nil, nil, nil, err + } + marketConfig := MarketConfig{ + Ticker: exchangeConfig.Ticker, + Invert: exchangeConfig.Invert, + } + + // Populate the adjustByMarket only if it is specified in the config. + if exchangeConfig.AdjustByMarket != "" { + adjustByMarketId, ok := marketNameToId[exchangeConfig.AdjustByMarket] + if !ok { + return nil, nil, nil, fmt.Errorf( + "unexpected internal error: invalid exchange config json for exchange '%v' "+ + "on market param %v: adjustByMarket '%v' not found", + exchangeConfig.ExchangeName, + marketParam.Id, + exchangeConfig.AdjustByMarket, + ) + } + marketConfig.AdjustByMarket = new(MarketId) + *marketConfig.AdjustByMarket = adjustByMarketId + } + mutableExchangeConfig.MarketToMarketConfig[marketParam.Id] = marketConfig + } + + // If we've reached this point, the market param is valid. Add it to the mutable market configs. + mutableMarketConfigs[marketParam.Id] = &MutableMarketConfig{ + Id: marketParam.Id, + Pair: marketParam.Pair, + Exponent: marketParam.Exponent, + MinExchanges: marketParam.MinExchanges, + } + } + return mutableExchangeConfigs, mutableMarketConfigs, marketParamErrors, nil +} + +// getUpdateParametersForExchange returns a copy of the exchange config and all relevant market configs for the +// exchange. This parameter list can be used to update the price fetcher or the price encoder whenever an exchange +// config changes. +func getUpdateParametersForExchange( + mutableExchangeConfig *MutableExchangeMarketConfig, + mutableMarketConfigs map[MarketId]*MutableMarketConfig, +) ( + updateParameters UpdateParameters, +) { + updateParameters.ExchangeConfig = mutableExchangeConfig.Copy() + + // Make a list of sorted copies of all market configurations for the exchange. + marketConfigCopies := make([]*MutableMarketConfig, 0, len(mutableExchangeConfig.MarketToMarketConfig)) + + // Detect which markets are needed. Due to adjustment markets, we need to deduplicate markets. + marketsOnExchange := make(map[MarketId]struct{}) + for marketId, config := range mutableExchangeConfig.MarketToMarketConfig { + marketsOnExchange[marketId] = struct{}{} + if config.AdjustByMarket != nil { + marketsOnExchange[*config.AdjustByMarket] = struct{}{} + } + } + + // Copy the market configs for each market on the exchange. + for marketId := range marketsOnExchange { + marketConfigCopies = append(marketConfigCopies, mutableMarketConfigs[marketId].Copy()) + } + + // Ensure markets are sorted by id in order to make behavior deterministic for testing. + sort.Slice(marketConfigCopies, func(i, j int) bool { + return marketConfigCopies[i].Id < marketConfigCopies[j].Id + }) + + updateParameters.MarketConfigs = marketConfigCopies + + return updateParameters +} + +// UpdateMarkets parses the market params, validates them, and updates the pricefeed mutable market configs, +// broadcasting updates to the price fetchers and encoders when necessary. +// This method is synchronized. +// 1. Validate and parse market params into a new set of MutableExchangeMarketConfig and MutableMarketConfig maps. +// 2. As a sanity check, validate all new configs have 2 entries: a price fetcher and encoder. +// 3. Pre-compute updates to send to updaters. +// 4. Take the writer lock on the pricefeed mutable market configs. +// 5. Swap in new markets and exchange configs. +// 6. For each changed exchange config, send an update to each updater. +// UpdateMarkets applies market settings independently. If any market param is invalid, the method will populate +// marketParamErrors with the error and continue processing the remaining market params. If the entire validation fails, +// the method will return an error. +func (pfmmc *PricefeedMutableMarketConfigsImpl) UpdateMarkets(marketParams []MarketParam) ( + marketParamErrors map[MarketId]error, + err error, +) { + // Wait for all updaters to be added. This should happen quickly after the daemon starts. + pfmmc.updatersInitialized.Wait() + + // Emit metrics periodically regardless of UpdateMarkets success/failure. + defer pfmmc.emitMarketAndExchangeCountMetrics() + + // 1. Validate and parse market params into a new mapping of MutableExchangeMarketConfigs and MutableMarketConfigs. + // maps. + if marketParams == nil { + return nil, fmt.Errorf("UpdateMarkets: marketParams cannot be nil") + } + + newMutableExchangeConfigs, + newMutableMarketConfigs, + marketParamErrors, + err := pfmmc.ValidateAndTransformParams(marketParams) + if err != nil { + return nil, fmt.Errorf("UpdateMarkets market param validation failed: %w", err) + } + + // 2. As a sanity check, validate all new configs have a set of updaters. + previousExchangeConfigs := pfmmc.mutableExchangeToConfigs + for exchangeId := range newMutableExchangeConfigs { + // Validate that a previous exchange config should always exist for each exchange. + // An error here would be unexpected. + if _, ok := previousExchangeConfigs[exchangeId]; !ok { + return nil, fmt.Errorf("internal error: exchange %v not found in previousExchangeConfigs", exchangeId) + } + + // Validate we have an encoder and fetcher subscribed for updates to each exchange. + // An error here would be unexpected. + if _, ok := pfmmc.mutableExchangeConfigUpdaters[exchangeId]; !ok { + return nil, fmt.Errorf("internal error: price fetcher not found for exchange %v", exchangeId) + } + + exchangeUpdaters := pfmmc.mutableExchangeConfigUpdaters[exchangeId] + if err := exchangeUpdaters.Validate(); err != nil { + return nil, fmt.Errorf("internal error for exchange %v: %w", exchangeId, err) + } + } + + // 3. Pre-compute updates to send to updaters. + updaterToUpdateParameters := make( + map[ExchangeConfigUpdater]UpdateParameters, + len(newMutableExchangeConfigs)*expectedUpdatersPerExchange, + ) + + for exchangeId, updaters := range pfmmc.mutableExchangeConfigUpdaters { + mutableExchangeConfig := newMutableExchangeConfigs[exchangeId] + previousConfig := previousExchangeConfigs[exchangeId] + + // If the exchange config has changed, pre-compute the updates to send to the price fetcher and encoder, which + // will be a copy of the updated exchange config, as well as a sorted list of copied market configs for each + // market on the exchange. + if !previousConfig.Equal(mutableExchangeConfig) { + updaterToUpdateParameters[updaters.PriceFetcher] = getUpdateParametersForExchange( + mutableExchangeConfig, + newMutableMarketConfigs, + ) + updaterToUpdateParameters[updaters.PriceEncoder] = getUpdateParametersForExchange( + mutableExchangeConfig, + newMutableMarketConfigs, + ) + } + } + + // 4. Take the writer lock. + pfmmc.Lock() + defer pfmmc.Unlock() + + // 5. Swap in new markets and exchange configs. + pfmmc.mutableExchangeToConfigs = newMutableExchangeConfigs + pfmmc.mutableMarketToConfigs = newMutableMarketConfigs + + // 6. For each changed exchange config, send an update to the associated price fetcher and encoder. + // Update the encoder before the fetcher so that the encoder has all necessary market config to support + // price updates coming from the fetcher in the event of adding a new market. + // TODO(DEC-2020): use errors.Join once it's available. + updaterErrors := make([]string, 0, len(updaterToUpdateParameters)) + + for exchangeId, updaters := range pfmmc.mutableExchangeConfigUpdaters { + // Update the encoder first. + if updateParams, ok := updaterToUpdateParameters[updaters.PriceEncoder]; ok { + err = updaters.PriceEncoder.UpdateMutableExchangeConfig(updateParams.ExchangeConfig, updateParams.MarketConfigs) + if err != nil { + updaterErrors = append( + updaterErrors, + fmt.Errorf( + "UpdateMarkets: failed to update price encoder for exchange %v: %w", + exchangeId, + err, + ).Error(), + ) + } + } + + // Update the fetcher second. + if updateParams, ok := updaterToUpdateParameters[updaters.PriceFetcher]; ok { + err = updaters.PriceFetcher.UpdateMutableExchangeConfig(updateParams.ExchangeConfig, updateParams.MarketConfigs) + if err != nil { + updaterErrors = append( + updaterErrors, + fmt.Errorf( + "UpdateMarkets: failed to update price fetcher for exchange %v: %w", + exchangeId, + err, + ).Error(), + ) + } + } + } + + if len(updaterErrors) > 0 { + err = fmt.Errorf("UpdateMarkets: failed to update some fetchers or encoders: %v", strings.Join(updaterErrors, ", ")) + } + + return marketParamErrors, err +} + +// GetExchangeMarketConfigCopy retrieves a copy of the current market-specific mutable configuration +// for all markets of an exchange, in order to maintain synchronization. Whenever a market is added +// or modified on an exchange, this data structure becomes stale. +func (pfmmc *PricefeedMutableMarketConfigsImpl) GetExchangeMarketConfigCopy( + id ExchangeId, +) ( + mutableExchangeMarketConfig *MutableExchangeMarketConfig, + err error, +) { + pfmmc.Lock() + defer pfmmc.Unlock() + memc, ok := pfmmc.mutableExchangeToConfigs[id] + if !ok { + return nil, fmt.Errorf("mutableExchangeMarketConfig not found for exchange %v", id) + } + return memc.Copy(), nil +} + +// GetMarketConfigCopies retrieves a copy of the current market-specific mutable configuration for +// the specified markets, in order to maintain synchronization. In the event of a market update, +// this data could become stale. MarketConfigs are returned in the same order as the input markets. +func (pfmmc *PricefeedMutableMarketConfigsImpl) GetMarketConfigCopies( + markets []MarketId, +) ( + mutableMarketConfigs []*MutableMarketConfig, + err error, +) { + pfmmc.Lock() + defer pfmmc.Unlock() + + mutableMarketConfigs = make([]*MutableMarketConfig, 0, len(markets)) + for _, market := range markets { + config, ok := pfmmc.mutableMarketToConfigs[market] + if !ok { + return nil, fmt.Errorf("market %v not found in mutableMarketToConfigs", market) + } + mutableMarketConfigs = append(mutableMarketConfigs, config.Copy()) + } + + return mutableMarketConfigs, nil +} + +// emitMarketAndExchangeCountMetrics emits metrics related to the number of configured markets and exchanges. +// This method is synchronized and invoked every time the pricefeed mutable market configs is updated. +// This method is not re-entrant and must be called via defer within other protected pricefeed mutable market +// config methods. +// Note: this method does not call into the metrics utility library shared by the daemon and the server because +// that library uses pricefeed constants, which has an import dependency on the types package here. The reason +// for this is that the MarketId, ExchangeId, and Exponent types are used to define the pricefeed constants +// for static config that is used within the types directory. The ultimate solution is to either remove this +// config or pass all of it +func (pfmmc *PricefeedMutableMarketConfigsImpl) emitMarketAndExchangeCountMetrics() { + pfmmc.Lock() + defer pfmmc.Unlock() + + // Set configured market count. + telemetry.SetGauge( + float32(len(pfmmc.mutableMarketToConfigs)), + metrics.PricefeedDaemon, + metrics.ConfiguredMarketCount, + ) + + for exchangeId, exchangeConfig := range pfmmc.mutableExchangeToConfigs { + // Report configured metric count with label. + telemetry.SetGaugeWithLabels( + []string{metrics.PricefeedDaemon, metrics.ConfiguredMarketCountPerExchange}, + float32(len(exchangeConfig.MarketToMarketConfig)), + []gometrics.Label{exchangeLabel(exchangeId)}, + ) + } + + // Set gauge for number of configured exchanges per market. + // Compute exchanges per market. + marketExchanges := make(map[MarketId][]ExchangeId, len(pfmmc.mutableMarketToConfigs)) + for marketId := range pfmmc.mutableMarketToConfigs { + marketExchanges[marketId] = make([]ExchangeId, 0, len(pfmmc.mutableExchangeToConfigs)) + } + for exchangeId, exchangeConfig := range pfmmc.mutableExchangeToConfigs { + for marketId := range exchangeConfig.MarketToMarketConfig { + marketExchanges[marketId] = append(marketExchanges[marketId], exchangeId) + } + } + // Report exchanges per market. + for marketId, exchanges := range marketExchanges { + // Report exchange count with market label. + telemetry.SetGaugeWithLabels( + []string{metrics.PricefeedDaemon, metrics.ConfiguredExchangeCountPerMarket}, + float32(len(exchanges)), + []gometrics.Label{marketLabel(pfmmc.mutableMarketToConfigs[marketId])}, + ) + } +} + +// exchangeLabel returns a metrics label for the specified exchange feed id. This logic is duplicated +// here to avoid import loops by introducing a dependency on the pricefeed constants. +func exchangeLabel(exchangeId ExchangeId) gometrics.Label { + return metrics.GetLabelForStringValue(metrics.ExchangeId, exchangeId) +} + +// marketLabel returns a metrics label for the specified market id. This logic is duplicated here to avoid +// import loops by introducing a dependency on the pricefeed constants. +func marketLabel(mutableMarketConfig *MutableMarketConfig) gometrics.Label { + if mutableMarketConfig == nil { + return metrics.GetLabelForStringValue(metrics.MarketId, "INVALID") + } + return metrics.GetLabelForStringValue(metrics.MarketId, mutableMarketConfig.Pair) +} diff --git a/daemons/pricefeed/metrics/market_pairs.go b/daemons/pricefeed/metrics/market_pairs.go new file mode 100644 index 000000000..0c4d7adf6 --- /dev/null +++ b/daemons/pricefeed/metrics/market_pairs.go @@ -0,0 +1,47 @@ +package metrics + +import ( + "fmt" + "sync" + + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + // marketToPair maps live marketIds to marketPair strings and is used for labelling metrics. + // This map is populated whenever markets are created or updated and access to this map is + // synchronized by the below mutex. + // The most correct approach here would be to get the marketPair from current chain state, so that + // we do not accidentally capture outdated or incorrect labels for market updates from rejected blocks. + // (A rejected block with a market creation should not present a problem as we will not log regular + // metrics for this market id unless it is later re-assigned - and re-updated.) However, these labels + // are used across the prices keeper, daemon, and daemon server code, so there is motive to avoid + // a solution that requires propagating references to centralized state. Furthermore, we judge that + // these market pairs are very unlikely to be updated, so this solution, while not perfect, is + // acceptable for the use case of logging/metrics in order to manage code complexity. + marketToPair = map[types.MarketId]string{} + // lock synchronizes access to the marketToPair map. + lock sync.Mutex +) + +// SetMarketPairForTelemetry sets a market pair to an in-memory map of marketId to marketPair strings used +// for labelling metrics. This method is synchronized. +func SetMarketPairForTelemetry(marketId types.MarketId, marketPair string) { + lock.Lock() + defer lock.Unlock() + marketToPair[marketId] = marketPair +} + +// GetMarketPairForTelemetry returns the market pair string for a given marketId. If the marketId is not +// found in the map, returns the INVALID string. This method is synchronized. +func GetMarketPairForTelemetry(marketId types.MarketId) string { + lock.Lock() + defer lock.Unlock() + + marketPair, exists := marketToPair[marketId] + if !exists { + return fmt.Sprintf("invalid_id:%v", marketId) + } + + return marketPair +} diff --git a/daemons/pricefeed/metrics/market_pairs_test.go b/daemons/pricefeed/metrics/market_pairs_test.go new file mode 100644 index 000000000..ba6c44838 --- /dev/null +++ b/daemons/pricefeed/metrics/market_pairs_test.go @@ -0,0 +1,31 @@ +package metrics_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/metrics" +) + +func TestGetMarketPairForTelemetry(t *testing.T) { + tests := map[string]struct { + marketId uint32 + expected string + }{ + "present id": { + marketId: 1, + expected: "BTC-USD", + }, + "absent id": { + marketId: 99, + expected: "invalid_id:99", + }, + } + metrics.SetMarketPairForTelemetry(1, "BTC-USD") + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := metrics.GetMarketPairForTelemetry(tc.marketId) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/daemons/pricefeed/metrics/metrics.go b/daemons/pricefeed/metrics/metrics.go new file mode 100644 index 000000000..4dded0011 --- /dev/null +++ b/daemons/pricefeed/metrics/metrics.go @@ -0,0 +1,24 @@ +package metrics + +import ( + gometrics "github.com/armon/go-metrics" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/lib/metrics" +) + +const ( + INVALID = "INVALID" +) + +// GetLabelForMarketId converts a marketId uint32 into a human-readable market pair and then returns the +// label with the market pair. +func GetLabelForMarketId(marketId types.MarketId) gometrics.Label { + marketPair := GetMarketPairForTelemetry(marketId) + return metrics.GetLabelForStringValue(metrics.MarketId, marketPair) +} + +// GetLabelForExchangeId converts an exchangeId uint32 into a name and then +// returns the label with the name. +func GetLabelForExchangeId(exchangeId types.ExchangeId) gometrics.Label { + return metrics.GetLabelForStringValue(metrics.ExchangeId, exchangeId) +} diff --git a/daemons/pricefeed/metrics/metrics_test.go b/daemons/pricefeed/metrics/metrics_test.go new file mode 100644 index 000000000..ec59b369c --- /dev/null +++ b/daemons/pricefeed/metrics/metrics_test.go @@ -0,0 +1,42 @@ +package metrics_test + +import ( + "fmt" + "testing" + + "github.com/tellor-io/layer/testutil/daemons/pricefeed/exchange_config" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/exchange_common" + pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" + "github.com/tellor-io/layer/lib/metrics" +) + +const ( + INVALID_ID = 10000000 +) + +func TestGetLabelForMarketIdSuccess(t *testing.T) { + pricefeedmetrics.SetMarketPairForTelemetry(exchange_config.MARKET_BTC_USD, "BTCUSD") + require.Equal( + t, + metrics.GetLabelForStringValue(metrics.MarketId, "BTCUSD"), + pricefeedmetrics.GetLabelForMarketId(exchange_config.MARKET_BTC_USD), + ) +} + +func TestGetLabelForMarketIdFailure(t *testing.T) { + require.Equal( + t, + metrics.GetLabelForStringValue(metrics.MarketId, fmt.Sprintf("invalid_id:%d", INVALID_ID)), + pricefeedmetrics.GetLabelForMarketId(INVALID_ID), + ) +} + +func TestGetLabelForExchangeId(t *testing.T) { + require.Equal( + t, + metrics.GetLabelForStringValue(metrics.ExchangeId, exchange_common.EXCHANGE_ID_BINANCE_US), + pricefeedmetrics.GetLabelForExchangeId(exchange_common.EXCHANGE_ID_BINANCE_US), + ) +} diff --git a/daemons/pricefeed/types/price_timestamp.go b/daemons/pricefeed/types/price_timestamp.go new file mode 100644 index 000000000..eeb58f7fb --- /dev/null +++ b/daemons/pricefeed/types/price_timestamp.go @@ -0,0 +1,38 @@ +package types + +import ( + "time" +) + +// PriceTimestamp maintains a price and its last update timestamp. +type PriceTimestamp struct { + LastUpdateTime time.Time + Price uint64 +} + +// NewPriceTimestamp creates a new PriceTimestamp. +func NewPriceTimestamp() *PriceTimestamp { + return &PriceTimestamp{} +} + +// UpdatePrice updates the price if the given update has a greater timestamp. Returns true if +// updating succeeds. Otherwise, returns false. +func (pt *PriceTimestamp) UpdatePrice(price uint64, newUpdateTime *time.Time) bool { + if newUpdateTime.After(pt.LastUpdateTime) { + pt.LastUpdateTime = *newUpdateTime + pt.Price = price + + return true + } + + return false +} + +// GetValidPrice returns (price, true) if the last update time is greater than or +// equal to the given cutoff time. Otherwise returns (0, false). +func (pt *PriceTimestamp) GetValidPrice(cutoffTime time.Time) (uint64, bool) { + if pt.LastUpdateTime.Before(cutoffTime) { + return 0, false + } + return pt.Price, true +} diff --git a/daemons/pricefeed/types/resolver.go b/daemons/pricefeed/types/resolver.go new file mode 100644 index 000000000..65a43cabf --- /dev/null +++ b/daemons/pricefeed/types/resolver.go @@ -0,0 +1,5 @@ +package types + +// Resolver is a function type that "resolves" a slice of values to a single value. +// The function also returns an error if there was an error in resolving the value. +type Resolver func([]uint64) (uint64, error) diff --git a/daemons/server/pricefeed.go b/daemons/server/pricefeed.go new file mode 100644 index 000000000..7a355bad4 --- /dev/null +++ b/daemons/server/pricefeed.go @@ -0,0 +1,124 @@ +package server + +import ( + "context" + "fmt" + "time" + + errorsmod "cosmossdk.io/errors" + + gometrics "github.com/armon/go-metrics" + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/constants" + pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" + + "github.com/tellor-io/layer/daemons/server/types" + pricefeedtypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" + "github.com/tellor-io/layer/lib/metrics" +) + +// PriceFeedServer defines the fields required for price updates. +type PriceFeedServer struct { + marketToExchange *pricefeedtypes.MarketToExchangePrices +} + +// WithPriceFeedMarketToExchangePrices sets the `MarketToExchangePrices` field. +// This is used by the price feed service to communicate price updates +// to the main application. +func (server *Server) WithPriceFeedMarketToExchangePrices( + marketToExchange *pricefeedtypes.MarketToExchangePrices, +) *Server { + server.marketToExchange = marketToExchange + return server +} + +// UpdateMarketPrices updates prices from exchanges for each market provided. +func (s *Server) UpdateMarketPrices( + ctx context.Context, + req *types.UpdateMarketPricesRequest, +) ( + response *types.UpdateMarketPricesResponse, + err error, +) { + // Measure latency in ingesting and handling gRPC price update. + defer telemetry.ModuleMeasureSince( + metrics.PricefeedServer, + time.Now(), + metrics.PricefeedServerUpdatePrices, + metrics.Latency, + ) + + // This panic is an unexpected condition because we initialize the market price cache in app initialization before + // starting the server or daemons. + if s.marketToExchange == nil { + panic(fmt.Errorf("server not initialized correclty, marketToExchange not initialized")) + } + + if err = validateMarketPricesUpdatesMessage(req); err != nil { + // Log if failure occurs during an update. + s.logger.Error("Failed to validate price update message", "error", err) + return nil, err + } + + s.marketToExchange.UpdatePrices(req.MarketPriceUpdates) + + // Capture valid responses in metrics. + s.reportValidResponse(types.PricefeedDaemonServiceName) + + return &types.UpdateMarketPricesResponse{MarketPriceUpdates: req.MarketPriceUpdates}, nil +} + +// validateMarketPricesUpdatesMessage validates a `UpdateMarketPricesRequest`. +func validateMarketPricesUpdatesMessage(req *types.UpdateMarketPricesRequest) error { + if len(req.MarketPriceUpdates) == 0 { + return fmt.Errorf("ErrPriceFeedMarketPriceUpdateEmpty") + } + + for _, mpu := range req.MarketPriceUpdates { + if err := validateMarketPriceUpdate(mpu); err != nil { + // Measure failure per market in validation. + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedServer, + metrics.PricefeedServerValidatePrices, + metrics.Error, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(mpu.MarketId), + }, + ) + + return err + } + } + + return nil +} + +// validateMarketPriceUpdate validates a single `MarketPriceUpdate`. +func validateMarketPriceUpdate(mpu *types.MarketPriceUpdate) error { + for _, ep := range mpu.ExchangePrices { + if ep.Price == constants.DefaultPrice { + return generateSdkErrorForExchangePrice( + fmt.Errorf("ErrPriceFeedInvalidPrice"), + ep, + mpu.MarketId, + ) + } + + if ep.LastUpdateTime == nil { + return generateSdkErrorForExchangePrice( + fmt.Errorf("ErrPriceFeedLastUpdateTimeNotSet"), + ep, + mpu.MarketId, + ) + } + } + return nil +} + +// generateSdkErrorForExchangePrice generates an error for an invalid `ExchangePrice`. +func generateSdkErrorForExchangePrice(err error, ep *types.ExchangePrice, marketId uint32) error { + return errorsmod.Wrapf(err, "ExchangePrice: %v and MarketId: %d", ep, marketId) +} diff --git a/daemons/server/pricefeed_test.go b/daemons/server/pricefeed_test.go new file mode 100644 index 000000000..9a5f7068b --- /dev/null +++ b/daemons/server/pricefeed_test.go @@ -0,0 +1,193 @@ +package server_test + +// import ( +// "context" +// "errors" +// "fmt" +// "testing" + +// errorsmod "cosmossdk.io/errors" +// pricefeed_types "github.com/tellor-io/layer/daemons/pricefeed/types" + +// "github.com/stretchr/testify/require" +// pricefeedconstants "github.com/tellor-io/layer/daemons/constants" +// "github.com/tellor-io/layer/daemons/pricefeed/api" +// "github.com/tellor-io/layer/daemons/server" +// pricefeedserver_types "github.com/tellor-io/layer/daemons/server/types/pricefeed" +// "github.com/tellor-io/layer/daemons/types" +// "github.com/tellor-io/layer/mocks" +// "github.com/tellor-io/layer/testutil/constants" +// ) + +// func TestUpdateMarketPrices_Valid(t *testing.T) { +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// s := createServerWithMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// ).WithPriceFeedMarketToExchangePrices( +// pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), +// ) + +// sendAndCheckPriceUpdate( +// t, +// s, +// &api.UpdateMarketPricesRequest{MarketPriceUpdates: constants.AtTimeTPriceUpdate}, +// nil, +// ) +// } + +// func TestUpdateMarketPrices_NotInitialized(t *testing.T) { +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// // Create a new server without initializing `MarketToExchange` field. +// s := createServerWithMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// ) + +// req := &api.UpdateMarketPricesRequest{MarketPriceUpdates: constants.AtTimeTPriceUpdate} +// require.PanicsWithError( +// t, +// errorsmod.Wrapf( +// types.ErrServerNotInitializedCorrectly, +// "MarketToExchange not initialized", +// ).Error(), +// func() { +// _, err := s.UpdateMarketPrices( +// context.TODO(), +// req, +// ) +// require.NoError(t, err, "No error expected when sending Price Update") +// }, +// ) +// } + +// func TestUpdateMarketPrices_InvalidEmptyRequest(t *testing.T) { +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// s := createServerWithMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// ).WithPriceFeedMarketToExchangePrices( +// pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), +// ) + +// sendAndCheckPriceUpdate( +// t, +// s, +// &api.UpdateMarketPricesRequest{MarketPriceUpdates: []*api.MarketPriceUpdate{}}, +// types.ErrPriceFeedMarketPriceUpdateEmpty, +// ) +// } + +// func TestUpdateMarketPrices_InvalidExchangePrices(t *testing.T) { +// tests := map[string]struct { +// input api.UpdateMarketPricesRequest +// expectedError error +// }{ +// "UpdateMarketPricesRequest Has ExchangePrice With 0 Price": { +// input: api.UpdateMarketPricesRequest{MarketPriceUpdates: []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// { +// ExchangeId: constants.ExchangeId1, +// Price: constants.InvalidPrice, +// LastUpdateTime: &constants.TimeT, +// }, +// }, +// }, +// }}, +// expectedError: fmt.Errorf( +// "Price is set to %d which is not a valid price", +// constants.InvalidPrice, +// ), +// }, +// "UpdateMarketPricesRequest Has ExchangePrice With Price Not Set": { +// input: api.UpdateMarketPricesRequest{MarketPriceUpdates: []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// { +// ExchangeId: constants.ExchangeId1, +// LastUpdateTime: &constants.TimeT, +// }, +// }, +// }, +// }}, +// expectedError: fmt.Errorf( +// "Price is set to %d which is not a valid price", +// pricefeedconstants.DefaultPrice, +// ), +// }, +// "UpdateMarketPricesRequest Has ExchangePrice With LastUpdatedAt Not Set": { +// input: api.UpdateMarketPricesRequest{MarketPriceUpdates: []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// { +// ExchangeId: constants.ExchangeId1, +// Price: constants.Price1, +// }, +// }, +// }, +// }}, +// expectedError: errors.New("LastUpdateTime is not set"), +// }, +// } + +// for name, tc := range tests { +// t.Run(name, func(t *testing.T) { +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// s := createServerWithMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// ).WithPriceFeedMarketToExchangePrices( +// pricefeedserver_types.NewMarketToExchangePrices(pricefeed_types.MaxPriceAge), +// ) +// expectedErr := errorsmod.Wrapf( +// tc.expectedError, +// "ExchangePrice: %v and MarketId: %d", +// // Assumes first ExchangePrice is the one with a validation error. +// tc.input.MarketPriceUpdates[0].ExchangePrices[0], +// tc.input.MarketPriceUpdates[0].MarketId, +// ) + +// sendAndCheckPriceUpdate(t, s, &tc.input, expectedErr) +// }) +// } +// } + +// func sendAndCheckPriceUpdate( +// t *testing.T, +// s *server.Server, +// req *api.UpdateMarketPricesRequest, +// expectedErr error, +// ) { +// apiResponse, err := s.UpdateMarketPrices( +// context.TODO(), +// req, +// ) + +// if expectedErr != nil { +// require.EqualError(t, err, expectedErr.Error()) +// } else { +// require.NoError(t, err, "No error expected when sending Price Update") +// require.Equal( +// t, +// &api.UpdateMarketPricesResponse{}, +// apiResponse, +// "response should be a pointer to a valid UpdateMarketPricesResponse", +// ) +// } +// } diff --git a/daemons/server/server.go b/daemons/server/server.go new file mode 100644 index 000000000..84552d619 --- /dev/null +++ b/daemons/server/server.go @@ -0,0 +1,104 @@ +package server + +import ( + "net" + + // "syscall" + gometrics "github.com/armon/go-metrics" + "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/server/types" + daemontypes "github.com/tellor-io/layer/daemons/types" + "github.com/tellor-io/layer/lib/metrics" +) + +// Server struct defines the shared gRPC server for all daemons. +// The struct contains fields related to gRPC server that are common to all daemons. +// In addition, the struct contains fields that are specific to various daemon services. +// needed for various services. +// to check queryServer implements ServiceServer + +type Server struct { + logger log.Logger + gsrv daemontypes.GrpcServer + fileHandler daemontypes.FileHandler + socketAddress string + PriceFeedServer +} + +// NewServer creates a single gRPC server that's shared across multiple daemons for communication. +// uniqueTestIdentifier is a string that can be used to identify services spawned by a particular test case, +// so that they can be cleaned up after the test case is complete. +func NewServer( + logger log.Logger, + grpcServer daemontypes.GrpcServer, + fileHandler daemontypes.FileHandler, + socketAddress string, +) *Server { + return &Server{ + logger: logger, + gsrv: grpcServer, + fileHandler: fileHandler, + socketAddress: socketAddress, + } +} + +// Stop stops the daemon server's gRPC service. +func (server *Server) Stop() { + server.gsrv.Stop() +} + +func (server *Server) ser() *Server { + return server +} + +// reportValidResponse reports a valid request/response from a daemon service for metrics collection purposes. +func (server *Server) reportValidResponse(daemonKey string) { + telemetry.IncrCounterWithLabels( + []string{ + metrics.DaemonServer, + metrics.ValidResponse, + }, + 1, + []gometrics.Label{ + metrics.GetLabelForStringValue(metrics.Daemon, daemonKey), + }, + ) +} + +// Start clears the current socket and establishes a new socket connection +// on the local filesystem. +// See URL for more information: https://eli.thegreenplace.net/2019/unix-domain-sockets-in-go/ +func (server *Server) Start() { + if err := server.fileHandler.RemoveAll(server.socketAddress); err != nil { + server.logger.Error("Failed to clear socket for daemon gRPC server", "error", err) + panic(err) + } + + // Restrict so that only user can read or write to socket generated by `net.Listen`. + // oldValue := syscall.Umask(constants.UmaskUserReadWriteOnly) + + ln, err := net.Listen(constants.UnixProtocol, server.socketAddress) + + // Restore umask bits back to previous value so that the entire process is not restricted to `UmaskUserReadWriteOnly`. + // syscall.Umask(oldValue) + + if err != nil { + server.logger.Error("Failed to listen to daemon gRPC server", "error", err) + panic(err) + } + + server.logger.Info("Daemon gRPC server is listening", "address", ln.Addr()) + + // Register gRPC services needed by the daemons. This is required before invoking `Serve`. + // https://pkg.go.dev/google.golang.org/grpc#Server.RegisterService + + // Register Server to ingest gRPC requests from price feed daemon and update market prices. + types.RegisterPriceFeedServiceServer(server.gsrv, server) + + if err := server.gsrv.Serve(ln); err != nil { + server.logger.Error("daemon gRPC server stopped with an error", "error", err) + panic(err) + } +} diff --git a/daemons/server/server_test.go b/daemons/server/server_test.go new file mode 100644 index 000000000..ac86b160e --- /dev/null +++ b/daemons/server/server_test.go @@ -0,0 +1,210 @@ +package server_test + +// import ( +// "errors" +// "fmt" +// "net" +// "os" +// "testing" + +// "github.com/cometbft/cometbft/libs/log" +// "github.com/stretchr/testify/mock" +// "github.com/stretchr/testify/require" +// pricefeedconstants "github.com/tellor-io/layer/daemons/constants" +// "github.com/tellor-io/layer/daemons/server" +// "github.com/tellor-io/layer/mocks" +// "github.com/tellor-io/layer/testutil/grpc" +// ) + +// const ( +// RemoveAllError = "RemovalAll ERROR!" +// ServeError = "Serve ERROR!" +// ) + +// func TestStartServer_ListenFailsWhenInUse(t *testing.T) { +// defer os.RemoveAll(grpc.SocketPath) + +// // Delete path to socket if it exists. +// err := os.RemoveAll(grpc.SocketPath) +// require.NoError(t, err, "No error should occur on clearing socket") + +// // Start Listening to socket. +// _, err = net.Listen(pricefeedconstants.UnixProtocol, grpc.SocketPath) +// require.NoError(t, err, "No error should occur on listening to socket") + +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// mockFileHandler.On("RemoveAll", grpc.SocketPath). +// Return(func(key string) error { +// return nil +// }) + +// s := createServerWithMocks(t, mockGrpcServer, mockFileHandler) + +// errorString := fmt.Sprintf( +// "listen %s %s: bind: address already in use", +// pricefeedconstants.UnixProtocol, +// grpc.SocketPath, +// ) +// require.PanicsWithError(t, errorString, s.Start) + +// verifyGrpcServerMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// true, +// false, +// ) +// } + +// func TestStart_Valid(t *testing.T) { +// // Remove filepath in case net.Listen is reached. +// defer os.RemoveAll(grpc.SocketPath) + +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// s := createServerWithMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// ) + +// mockFileHandler.On("RemoveAll", grpc.SocketPath). +// Return(nil) + +// mockGrpcServer.On("Serve", mock.Anything). +// Return(nil) +// mockGrpcServer.On("RegisterService", mock.Anything, mock.Anything). +// Return() + +// require.NotPanics(t, s.Start) + +// // Reset with Umask. +// verifyFilePermissions(t, grpc.SocketPath, grpc.DefaultPermissions) + +// verifyGrpcServerMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// true, +// true, +// ) +// } + +// func TestStart_MixedInvalid(t *testing.T) { +// tests := map[string]struct { +// FileHandlerror error +// serveError error +// }{ +// "Remove Socket Fails": { +// FileHandlerror: errors.New(RemoveAllError), +// }, +// "Serve Fails": { +// serveError: errors.New(ServeError), +// }, +// } + +// for name, tc := range tests { +// t.Run(name, func(t *testing.T) { +// // Remove filepath in case net.Listen is reached. +// defer os.RemoveAll(grpc.SocketPath) + +// mockGrpcServer := &mocks.GrpcServer{} +// mockFileHandler := &mocks.FileHandler{} + +// s := createServerWithMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// ) + +// var expectedError error + +// expectedSocketPermissions := grpc.UserReadWriteOnlyPermissions + +// if tc.FileHandlerror != nil { +// expectedError = tc.FileHandlerror +// } +// mockFileHandler.On("RemoveAll", grpc.SocketPath). +// Return(tc.FileHandlerror) + +// if tc.serveError != nil { +// expectedSocketPermissions = grpc.DefaultPermissions + +// expectedError = tc.serveError +// } + +// mockGrpcServer.On("RegisterService", mock.Anything, mock.Anything). +// Return() +// mockGrpcServer.On("Serve", mock.Anything). +// Return(tc.serveError) + +// require.PanicsWithError(t, expectedError.Error(), s.Start) + +// if tc.serveError != nil { +// // Failed to reset with Umask before panicking. +// verifyFilePermissions(t, grpc.SocketPath, expectedSocketPermissions) +// } + +// verifyGrpcServerMocks( +// t, +// mockGrpcServer, +// mockFileHandler, +// true, +// tc.serveError != nil, +// ) +// }) +// } +// } + +// func createServerWithMocks( +// t testing.TB, +// mockGrpcServer *mocks.GrpcServer, +// mockFileHandler *mocks.FileHandler, +// ) *server.Server { +// server := server.NewServer( +// log.NewNopLogger(), +// mockGrpcServer, +// mockFileHandler, +// grpc.SocketPath, +// ) +// mockGrpcServer.On("Stop").Return().Once() +// t.Cleanup(server.Stop) +// return server +// } + +// func verifyFilePermissions( +// t *testing.T, +// path string, +// expectedPermissions os.FileMode, +// ) { +// fileStats, err := os.Stat(path) +// require.NoError(t, err, "Stats should exist for file at path") + +// permissions := fileStats.Mode().Perm() +// require.Equal(t, expectedPermissions, permissions) +// } + +// func verifyGrpcServerMocks( +// t *testing.T, +// mockGrpcServer *mocks.GrpcServer, +// mockFileHandler *mocks.FileHandler, +// removeAllIsCalled bool, +// serveIsCalled bool, +// ) { +// if removeAllIsCalled { +// mockFileHandler.AssertCalled(t, "RemoveAll", grpc.SocketPath) +// } else { +// mockFileHandler.AssertNotCalled(t, "RemoveAll", grpc.SocketPath) +// } + +// if serveIsCalled { +// mockGrpcServer.AssertCalled(t, "Serve", mock.Anything) +// mockGrpcServer.AssertCalled(t, "RegisterService", mock.Anything, mock.Anything) +// } else { +// mockGrpcServer.AssertNotCalled(t, "Serve", mock.Anything) +// mockGrpcServer.AssertNotCalled(t, "RegisterService", mock.Anything, mock.Anything) +// } +// } diff --git a/daemons/server/types/constants.go b/daemons/server/types/constants.go new file mode 100644 index 000000000..8fd75028d --- /dev/null +++ b/daemons/server/types/constants.go @@ -0,0 +1,17 @@ +package types + +import "time" + +const ( + // DaemonStartupGracePeriod is the amount of time to wait for before a daemon is expected to start querying + // the daemon server. This is used to ensure that spurious panics aren't produced due to the daemons waiting for + // the cosmos grpc service to start. If cli tests are failing due to panics because it is taking the network + // a long time to start the protocol, it's possible this value could be increased. + DaemonStartupGracePeriod = 60 * time.Second + + // MaximumLoopDelayMultiple defines the maximum acceptable update delay for a daemon as a multiple of the + // daemon's loop delay. + MaximumLoopDelayMultiple = 3 + + PricefeedDaemonServiceName = "pricefeed-daemon" +) diff --git a/daemons/server/types/pricefeed.pb.go b/daemons/server/types/pricefeed.pb.go new file mode 100644 index 000000000..8ae0de750 --- /dev/null +++ b/daemons/server/types/pricefeed.pb.go @@ -0,0 +1,1100 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: layer/daemons/pricefeed.proto + +package types + +import ( + context "context" + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + _ "google.golang.org/protobuf/types/known/timestamppb" + io "io" + math "math" + math_bits "math/bits" + time "time" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf +var _ = time.Kitchen + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// UpdateMarketPriceRequest is a request message updating market prices. +type UpdateMarketPricesRequest struct { + MarketPriceUpdates []*MarketPriceUpdate `protobuf:"bytes,1,rep,name=market_price_updates,json=marketPriceUpdates,proto3" json:"market_price_updates,omitempty"` +} + +func (m *UpdateMarketPricesRequest) Reset() { *m = UpdateMarketPricesRequest{} } +func (m *UpdateMarketPricesRequest) String() string { return proto.CompactTextString(m) } +func (*UpdateMarketPricesRequest) ProtoMessage() {} +func (*UpdateMarketPricesRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f18ab54548ab09c4, []int{0} +} +func (m *UpdateMarketPricesRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *UpdateMarketPricesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_UpdateMarketPricesRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *UpdateMarketPricesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_UpdateMarketPricesRequest.Merge(m, src) +} +func (m *UpdateMarketPricesRequest) XXX_Size() int { + return m.Size() +} +func (m *UpdateMarketPricesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_UpdateMarketPricesRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_UpdateMarketPricesRequest proto.InternalMessageInfo + +func (m *UpdateMarketPricesRequest) GetMarketPriceUpdates() []*MarketPriceUpdate { + if m != nil { + return m.MarketPriceUpdates + } + return nil +} + +// UpdateMarketPricesResponse is a response message for updating market prices. +type UpdateMarketPricesResponse struct { + MarketPriceUpdates []*MarketPriceUpdate `protobuf:"bytes,1,rep,name=market_price_updates,json=marketPriceUpdates,proto3" json:"market_price_updates,omitempty"` +} + +func (m *UpdateMarketPricesResponse) Reset() { *m = UpdateMarketPricesResponse{} } +func (m *UpdateMarketPricesResponse) String() string { return proto.CompactTextString(m) } +func (*UpdateMarketPricesResponse) ProtoMessage() {} +func (*UpdateMarketPricesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f18ab54548ab09c4, []int{1} +} +func (m *UpdateMarketPricesResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *UpdateMarketPricesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_UpdateMarketPricesResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *UpdateMarketPricesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_UpdateMarketPricesResponse.Merge(m, src) +} +func (m *UpdateMarketPricesResponse) XXX_Size() int { + return m.Size() +} +func (m *UpdateMarketPricesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_UpdateMarketPricesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_UpdateMarketPricesResponse proto.InternalMessageInfo + +func (m *UpdateMarketPricesResponse) GetMarketPriceUpdates() []*MarketPriceUpdate { + if m != nil { + return m.MarketPriceUpdates + } + return nil +} + +// ExchangePrice represents a specific exchange's market price +type ExchangePrice struct { + ExchangeId string `protobuf:"bytes,1,opt,name=exchange_id,json=exchangeId,proto3" json:"exchange_id,omitempty"` + Price uint64 `protobuf:"varint,2,opt,name=price,proto3" json:"price,omitempty"` + LastUpdateTime *time.Time `protobuf:"bytes,3,opt,name=last_update_time,json=lastUpdateTime,proto3,stdtime" json:"last_update_time,omitempty"` +} + +func (m *ExchangePrice) Reset() { *m = ExchangePrice{} } +func (m *ExchangePrice) String() string { return proto.CompactTextString(m) } +func (*ExchangePrice) ProtoMessage() {} +func (*ExchangePrice) Descriptor() ([]byte, []int) { + return fileDescriptor_f18ab54548ab09c4, []int{2} +} +func (m *ExchangePrice) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ExchangePrice) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ExchangePrice.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ExchangePrice) XXX_Merge(src proto.Message) { + xxx_messageInfo_ExchangePrice.Merge(m, src) +} +func (m *ExchangePrice) XXX_Size() int { + return m.Size() +} +func (m *ExchangePrice) XXX_DiscardUnknown() { + xxx_messageInfo_ExchangePrice.DiscardUnknown(m) +} + +var xxx_messageInfo_ExchangePrice proto.InternalMessageInfo + +func (m *ExchangePrice) GetExchangeId() string { + if m != nil { + return m.ExchangeId + } + return "" +} + +func (m *ExchangePrice) GetPrice() uint64 { + if m != nil { + return m.Price + } + return 0 +} + +func (m *ExchangePrice) GetLastUpdateTime() *time.Time { + if m != nil { + return m.LastUpdateTime + } + return nil +} + +// MarketPriceUpdate represents an update to a single market +type MarketPriceUpdate struct { + MarketId uint32 `protobuf:"varint,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ExchangePrices []*ExchangePrice `protobuf:"bytes,2,rep,name=exchange_prices,json=exchangePrices,proto3" json:"exchange_prices,omitempty"` +} + +func (m *MarketPriceUpdate) Reset() { *m = MarketPriceUpdate{} } +func (m *MarketPriceUpdate) String() string { return proto.CompactTextString(m) } +func (*MarketPriceUpdate) ProtoMessage() {} +func (*MarketPriceUpdate) Descriptor() ([]byte, []int) { + return fileDescriptor_f18ab54548ab09c4, []int{3} +} +func (m *MarketPriceUpdate) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MarketPriceUpdate) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MarketPriceUpdate.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MarketPriceUpdate) XXX_Merge(src proto.Message) { + xxx_messageInfo_MarketPriceUpdate.Merge(m, src) +} +func (m *MarketPriceUpdate) XXX_Size() int { + return m.Size() +} +func (m *MarketPriceUpdate) XXX_DiscardUnknown() { + xxx_messageInfo_MarketPriceUpdate.DiscardUnknown(m) +} + +var xxx_messageInfo_MarketPriceUpdate proto.InternalMessageInfo + +func (m *MarketPriceUpdate) GetMarketId() uint32 { + if m != nil { + return m.MarketId + } + return 0 +} + +func (m *MarketPriceUpdate) GetExchangePrices() []*ExchangePrice { + if m != nil { + return m.ExchangePrices + } + return nil +} + +func init() { + proto.RegisterType((*UpdateMarketPricesRequest)(nil), "layer.daemons.UpdateMarketPricesRequest") + proto.RegisterType((*UpdateMarketPricesResponse)(nil), "layer.daemons.UpdateMarketPricesResponse") + proto.RegisterType((*ExchangePrice)(nil), "layer.daemons.ExchangePrice") + proto.RegisterType((*MarketPriceUpdate)(nil), "layer.daemons.MarketPriceUpdate") +} + +func init() { proto.RegisterFile("layer/daemons/pricefeed.proto", fileDescriptor_f18ab54548ab09c4) } + +var fileDescriptor_f18ab54548ab09c4 = []byte{ + // 436 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x53, 0x41, 0x6f, 0xd3, 0x30, + 0x14, 0xae, 0x37, 0x40, 0x9b, 0xab, 0x8e, 0x61, 0xf5, 0x10, 0xc2, 0x48, 0xa3, 0x9c, 0xc2, 0x01, + 0x5b, 0x2a, 0xff, 0x60, 0xd2, 0x90, 0x7a, 0x00, 0x21, 0x03, 0x17, 0x2e, 0x91, 0xdb, 0xbc, 0x65, + 0xd1, 0x92, 0xd8, 0xd8, 0xce, 0x60, 0x27, 0xfe, 0xc2, 0x4e, 0xfc, 0xa6, 0x1d, 0x77, 0xe4, 0x04, + 0xa8, 0xfd, 0x23, 0x28, 0x76, 0x8a, 0xd6, 0x0e, 0x24, 0x2e, 0xdc, 0xfc, 0xbe, 0xef, 0xcb, 0x7b, + 0xdf, 0xfb, 0x1c, 0xe3, 0xa7, 0x95, 0xb8, 0x04, 0xcd, 0x72, 0x01, 0xb5, 0x6c, 0x0c, 0x53, 0xba, + 0x5c, 0xc0, 0x29, 0x40, 0x4e, 0x95, 0x96, 0x56, 0x92, 0x91, 0xa3, 0x69, 0x4f, 0x87, 0xe3, 0x42, + 0x16, 0xd2, 0x31, 0xac, 0x3b, 0x79, 0x51, 0x78, 0x54, 0x48, 0x59, 0x54, 0xc0, 0x84, 0x2a, 0x99, + 0x68, 0x1a, 0x69, 0x85, 0x2d, 0x65, 0x63, 0x7a, 0x76, 0xd2, 0xb3, 0xae, 0x9a, 0xb7, 0xa7, 0xcc, + 0x96, 0x35, 0x18, 0x2b, 0x6a, 0xe5, 0x05, 0x89, 0xc4, 0x8f, 0xdf, 0xab, 0x5c, 0x58, 0x78, 0x25, + 0xf4, 0x39, 0xd8, 0x37, 0x9d, 0x05, 0xc3, 0xe1, 0x63, 0x0b, 0xc6, 0x12, 0x8e, 0xc7, 0xb5, 0x83, + 0x33, 0x67, 0x2d, 0x6b, 0x9d, 0xd2, 0x04, 0x28, 0xde, 0x4d, 0x87, 0xd3, 0x98, 0x6e, 0xf8, 0xa3, + 0xb7, 0x3a, 0xf8, 0x96, 0x9c, 0xd4, 0xdb, 0x90, 0x49, 0x14, 0x0e, 0xff, 0x34, 0xd0, 0x28, 0xd9, + 0x18, 0xf8, 0x2f, 0x13, 0xbf, 0x22, 0x3c, 0x3a, 0xf9, 0xbc, 0x38, 0x13, 0x4d, 0x01, 0x8e, 0x20, + 0x13, 0x3c, 0x84, 0x1e, 0xc8, 0xca, 0x3c, 0x40, 0x31, 0x4a, 0xf7, 0x39, 0x5e, 0x43, 0xb3, 0x9c, + 0x8c, 0xf1, 0x7d, 0x37, 0x3f, 0xd8, 0x89, 0x51, 0x7a, 0x8f, 0xfb, 0x82, 0xbc, 0xc6, 0x87, 0x95, + 0x30, 0xb6, 0x37, 0x95, 0x75, 0x51, 0x06, 0xbb, 0x31, 0x4a, 0x87, 0xd3, 0x90, 0xfa, 0x9c, 0xe9, + 0x3a, 0x67, 0xfa, 0x6e, 0x9d, 0xf3, 0xf1, 0xde, 0xf5, 0xf7, 0x09, 0xba, 0xfa, 0x31, 0x41, 0xfc, + 0xa0, 0xfb, 0xda, 0xbb, 0xea, 0xe8, 0xe4, 0x13, 0x7e, 0x74, 0x67, 0x03, 0xf2, 0x04, 0xef, 0xf7, + 0x09, 0xf4, 0xce, 0x46, 0x7c, 0xcf, 0x03, 0xb3, 0x9c, 0x9c, 0xe0, 0x87, 0xbf, 0x8d, 0x3b, 0x4f, + 0x26, 0xd8, 0x71, 0xc9, 0x1c, 0x6d, 0x25, 0xb3, 0xb1, 0x2f, 0x3f, 0x80, 0xdb, 0xa5, 0x99, 0x7e, + 0xc1, 0x87, 0xee, 0xf4, 0x12, 0x20, 0x7f, 0x0b, 0xfa, 0xa2, 0x5b, 0xee, 0x1c, 0x93, 0xbb, 0xf7, + 0x42, 0xd2, 0xad, 0xbe, 0x7f, 0xfd, 0x57, 0xc2, 0x67, 0xff, 0xa0, 0xf4, 0x97, 0x9c, 0x0c, 0x8e, + 0x67, 0xd7, 0xcb, 0x08, 0xdd, 0x2c, 0x23, 0xf4, 0x73, 0x19, 0xa1, 0xab, 0x55, 0x34, 0xb8, 0x59, + 0x45, 0x83, 0x6f, 0xab, 0x68, 0xf0, 0x81, 0x15, 0xa5, 0x3d, 0x6b, 0xe7, 0x74, 0x21, 0x6b, 0x66, + 0xa1, 0xaa, 0xa4, 0x7e, 0x5e, 0x4a, 0xb6, 0xf9, 0x4e, 0x0c, 0xe8, 0x0b, 0xd0, 0xcc, 0x5e, 0x2a, + 0x30, 0xf3, 0x07, 0x2e, 0xf2, 0x17, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x43, 0x3e, 0x3a, 0x18, + 0x4c, 0x03, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// PriceFeedServiceClient is the client API for PriceFeedService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type PriceFeedServiceClient interface { + // Updates market prices. + UpdateMarketPrices(ctx context.Context, in *UpdateMarketPricesRequest, opts ...grpc.CallOption) (*UpdateMarketPricesResponse, error) +} + +type priceFeedServiceClient struct { + cc grpc1.ClientConn +} + +func NewPriceFeedServiceClient(cc grpc1.ClientConn) PriceFeedServiceClient { + return &priceFeedServiceClient{cc} +} + +func (c *priceFeedServiceClient) UpdateMarketPrices(ctx context.Context, in *UpdateMarketPricesRequest, opts ...grpc.CallOption) (*UpdateMarketPricesResponse, error) { + out := new(UpdateMarketPricesResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.PriceFeedService/UpdateMarketPrices", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PriceFeedServiceServer is the server API for PriceFeedService service. +type PriceFeedServiceServer interface { + // Updates market prices. + UpdateMarketPrices(context.Context, *UpdateMarketPricesRequest) (*UpdateMarketPricesResponse, error) +} + +// UnimplementedPriceFeedServiceServer can be embedded to have forward compatible implementations. +type UnimplementedPriceFeedServiceServer struct { +} + +func (*UnimplementedPriceFeedServiceServer) UpdateMarketPrices(ctx context.Context, req *UpdateMarketPricesRequest) (*UpdateMarketPricesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateMarketPrices not implemented") +} + +func RegisterPriceFeedServiceServer(s grpc1.Server, srv PriceFeedServiceServer) { + s.RegisterService(&_PriceFeedService_serviceDesc, srv) +} + +func _PriceFeedService_UpdateMarketPrices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateMarketPricesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PriceFeedServiceServer).UpdateMarketPrices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.daemons.PriceFeedService/UpdateMarketPrices", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PriceFeedServiceServer).UpdateMarketPrices(ctx, req.(*UpdateMarketPricesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _PriceFeedService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "layer.daemons.PriceFeedService", + HandlerType: (*PriceFeedServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UpdateMarketPrices", + Handler: _PriceFeedService_UpdateMarketPrices_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "layer/daemons/pricefeed.proto", +} + +func (m *UpdateMarketPricesRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *UpdateMarketPricesRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *UpdateMarketPricesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.MarketPriceUpdates) > 0 { + for iNdEx := len(m.MarketPriceUpdates) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MarketPriceUpdates[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintPricefeed(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *UpdateMarketPricesResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *UpdateMarketPricesResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *UpdateMarketPricesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.MarketPriceUpdates) > 0 { + for iNdEx := len(m.MarketPriceUpdates) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MarketPriceUpdates[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintPricefeed(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *ExchangePrice) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ExchangePrice) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ExchangePrice) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.LastUpdateTime != nil { + n1, err1 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.LastUpdateTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastUpdateTime):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintPricefeed(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x1a + } + if m.Price != 0 { + i = encodeVarintPricefeed(dAtA, i, uint64(m.Price)) + i-- + dAtA[i] = 0x10 + } + if len(m.ExchangeId) > 0 { + i -= len(m.ExchangeId) + copy(dAtA[i:], m.ExchangeId) + i = encodeVarintPricefeed(dAtA, i, uint64(len(m.ExchangeId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MarketPriceUpdate) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MarketPriceUpdate) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MarketPriceUpdate) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.ExchangePrices) > 0 { + for iNdEx := len(m.ExchangePrices) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ExchangePrices[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintPricefeed(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if m.MarketId != 0 { + i = encodeVarintPricefeed(dAtA, i, uint64(m.MarketId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintPricefeed(dAtA []byte, offset int, v uint64) int { + offset -= sovPricefeed(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *UpdateMarketPricesRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.MarketPriceUpdates) > 0 { + for _, e := range m.MarketPriceUpdates { + l = e.Size() + n += 1 + l + sovPricefeed(uint64(l)) + } + } + return n +} + +func (m *UpdateMarketPricesResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.MarketPriceUpdates) > 0 { + for _, e := range m.MarketPriceUpdates { + l = e.Size() + n += 1 + l + sovPricefeed(uint64(l)) + } + } + return n +} + +func (m *ExchangePrice) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ExchangeId) + if l > 0 { + n += 1 + l + sovPricefeed(uint64(l)) + } + if m.Price != 0 { + n += 1 + sovPricefeed(uint64(m.Price)) + } + if m.LastUpdateTime != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.LastUpdateTime) + n += 1 + l + sovPricefeed(uint64(l)) + } + return n +} + +func (m *MarketPriceUpdate) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.MarketId != 0 { + n += 1 + sovPricefeed(uint64(m.MarketId)) + } + if len(m.ExchangePrices) > 0 { + for _, e := range m.ExchangePrices { + l = e.Size() + n += 1 + l + sovPricefeed(uint64(l)) + } + } + return n +} + +func sovPricefeed(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozPricefeed(x uint64) (n int) { + return sovPricefeed(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *UpdateMarketPricesRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: UpdateMarketPricesRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: UpdateMarketPricesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MarketPriceUpdates", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPricefeed + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthPricefeed + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MarketPriceUpdates = append(m.MarketPriceUpdates, &MarketPriceUpdate{}) + if err := m.MarketPriceUpdates[len(m.MarketPriceUpdates)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPricefeed(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthPricefeed + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *UpdateMarketPricesResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: UpdateMarketPricesResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: UpdateMarketPricesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MarketPriceUpdates", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPricefeed + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthPricefeed + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MarketPriceUpdates = append(m.MarketPriceUpdates, &MarketPriceUpdate{}) + if err := m.MarketPriceUpdates[len(m.MarketPriceUpdates)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPricefeed(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthPricefeed + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ExchangePrice) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ExchangePrice: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ExchangePrice: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ExchangeId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPricefeed + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthPricefeed + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ExchangeId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) + } + m.Price = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Price |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastUpdateTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPricefeed + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthPricefeed + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.LastUpdateTime == nil { + m.LastUpdateTime = new(time.Time) + } + if err := github_com_cosmos_gogoproto_types.StdTimeUnmarshal(m.LastUpdateTime, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPricefeed(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthPricefeed + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MarketPriceUpdate) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MarketPriceUpdate: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MarketPriceUpdate: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MarketId", wireType) + } + m.MarketId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MarketId |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ExchangePrices", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPricefeed + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPricefeed + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthPricefeed + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ExchangePrices = append(m.ExchangePrices, &ExchangePrice{}) + if err := m.ExchangePrices[len(m.ExchangePrices)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPricefeed(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthPricefeed + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipPricefeed(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowPricefeed + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowPricefeed + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowPricefeed + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthPricefeed + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupPricefeed + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthPricefeed + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthPricefeed = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowPricefeed = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupPricefeed = fmt.Errorf("proto: unexpected end of group") +) diff --git a/daemons/server/types/pricefeed/exchange_to_price.go b/daemons/server/types/pricefeed/exchange_to_price.go new file mode 100644 index 000000000..f8a9600e2 --- /dev/null +++ b/daemons/server/types/pricefeed/exchange_to_price.go @@ -0,0 +1,95 @@ +package types + +import ( + "time" + + gometrics "github.com/armon/go-metrics" + "github.com/cosmos/cosmos-sdk/telemetry" + pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" + "github.com/tellor-io/layer/daemons/pricefeed/types" + servertypes "github.com/tellor-io/layer/daemons/server/types" + "github.com/tellor-io/layer/lib/metrics" +) + +// ExchangeToPrice maintains multiple prices from different exchanges for +// the same market, along with the last time the each exchange price was updated. +type ExchangeToPrice struct { + marketId uint32 + exchangeToPriceTimestamp map[string]*types.PriceTimestamp +} + +// NewExchangeToPrice creates a new ExchangeToPrice. It takes a market ID, which is used in logging and metrics to +// identify the market these exchange prices are for. The market ID does not otherwise affect the behavior +// of the ExchangeToPrice. +func NewExchangeToPrice(marketId uint32) *ExchangeToPrice { + return &ExchangeToPrice{ + marketId: marketId, + exchangeToPriceTimestamp: make(map[string]*types.PriceTimestamp), + } +} + +// UpdatePrices updates prices given a list of prices from different exchanges. +// Prices are only updated if the timestamp on the updates are greater than +// the timestamp on existing prices. +func (etp *ExchangeToPrice) UpdatePrices(updates []*servertypes.ExchangePrice) { + for _, exchangePrice := range updates { + exchangeId := exchangePrice.ExchangeId + priceTimestamp, exists := etp.exchangeToPriceTimestamp[exchangeId] + if !exists { + priceTimestamp = types.NewPriceTimestamp() + etp.exchangeToPriceTimestamp[exchangeId] = priceTimestamp + } + + isUpdated := priceTimestamp.UpdatePrice(exchangePrice.Price, exchangePrice.LastUpdateTime) + + validity := metrics.Valid + if exists && !isUpdated { + validity = metrics.Invalid + } + + // Measure count of valid and invalid prices inserted into the in-memory map. + telemetry.IncrCounterWithLabels( + []string{metrics.PricefeedServer, metrics.UpdatePrice, validity, metrics.Count}, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(etp.marketId), + pricefeedmetrics.GetLabelForExchangeId(exchangeId), + }, + ) + } +} + +// GetValidPrices returns a list of "valid" prices. Prices are considered +// "valid" iff the last update time is greater than or equal to the given cutoff time. +func (etp *ExchangeToPrice) GetValidPrices( + cutoffTime time.Time, +) []uint64 { + validExchangePricesForMarket := make([]uint64, 0, len(etp.exchangeToPriceTimestamp)) + for exchangeId, priceTimestamp := range etp.exchangeToPriceTimestamp { + validity := metrics.Valid + + // PriceTimestamp returns price if the last update time is valid. + if price, ok := priceTimestamp.GetValidPrice(cutoffTime); ok { + validExchangePricesForMarket = append(validExchangePricesForMarket, price) + } else { + // Price is invalid. + validity = metrics.PriceIsInvalid + } + + // Measure count of valid and invalid prices fetched from the in-memory map. + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedServer, + metrics.GetValidPrices, + validity, + metrics.Count, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForExchangeId(exchangeId), + pricefeedmetrics.GetLabelForMarketId(etp.marketId), + }, + ) + } + return validExchangePricesForMarket +} diff --git a/daemons/server/types/pricefeed/exchange_to_price_test.go b/daemons/server/types/pricefeed/exchange_to_price_test.go new file mode 100644 index 000000000..eca2d0ac1 --- /dev/null +++ b/daemons/server/types/pricefeed/exchange_to_price_test.go @@ -0,0 +1,157 @@ +package types + +// import ( +// "testing" + +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" +// "github.com/tellor-io/layer/daemons/pricefeed/api" +// "github.com/tellor-io/layer/testutil/constants" +// ) + +// func TestNewExchangeToPrices_IsEmpty(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// require.Empty(t, etp.exchangeToPriceTimestamp) +// } + +// func TestUpdatePrices_SingleExchangeSingleUpdate(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// }) + +// require.Len(t, etp.exchangeToPriceTimestamp, 1) +// priceTimestamp, ok := etp.exchangeToPriceTimestamp[constants.ExchangeId1] +// require.True(t, ok) +// require.Equal(t, priceTimestamp.Price, constants.Price1) +// require.Equal(t, priceTimestamp.LastUpdateTime, constants.TimeT) +// } + +// func TestUpdatePrices_SingleExchangeMultiUpdate(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange1_Price2_AfterTimeT, +// }) + +// // Update with greater timestamp overwrites +// require.Len(t, etp.exchangeToPriceTimestamp, 1) +// priceTimestamp := etp.exchangeToPriceTimestamp[constants.ExchangeId1] +// require.Equal(t, priceTimestamp.Price, constants.Price2) +// require.Equal(t, priceTimestamp.LastUpdateTime, constants.TimeTPlusThreshold) +// } + +// func TestUpdatePrices_MultiExchangeSingleUpdate(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }) + +// require.Len(t, etp.exchangeToPriceTimestamp, 2) +// priceTimestamp1 := etp.exchangeToPriceTimestamp[constants.ExchangeId1] +// priceTimestamp2 := etp.exchangeToPriceTimestamp[constants.ExchangeId2] +// require.Equal(t, priceTimestamp1.Price, constants.Price1) +// require.Equal(t, priceTimestamp1.LastUpdateTime, constants.TimeT) +// require.Equal(t, priceTimestamp2.Price, constants.Price2) +// require.Equal(t, priceTimestamp2.LastUpdateTime, constants.TimeT) +// } + +// func TestUpdatePrices_MultiExchangeMutliUpdate(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange1_Price2_AfterTimeT, +// constants.Exchange2_Price2_TimeT, +// constants.Exchange2_Price3_AfterTimeT, +// }) + +// // Update with greater timestamp overwrites +// require.Len(t, etp.exchangeToPriceTimestamp, 2) +// priceTimestamp1 := etp.exchangeToPriceTimestamp[constants.ExchangeId1] +// priceTimestamp2 := etp.exchangeToPriceTimestamp[constants.ExchangeId2] +// require.Equal(t, priceTimestamp1.Price, constants.Price2) +// require.Equal(t, priceTimestamp1.LastUpdateTime, constants.TimeTPlusThreshold) +// require.Equal(t, priceTimestamp2.Price, constants.Price3) +// require.Equal(t, priceTimestamp2.LastUpdateTime, constants.TimeTPlusThreshold) +// } + +// func TestUpdatePrices_OldUpdateFails(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// }) +// priceTimestamp1 := etp.exchangeToPriceTimestamp[constants.ExchangeId1] + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price3_BeforeTimeT, +// }) + +// // Older timestamp does NOT update the prices. +// require.Len(t, etp.exchangeToPriceTimestamp, 1) +// require.Equal(t, priceTimestamp1.Price, constants.Price1) +// require.Equal(t, priceTimestamp1.LastUpdateTime, constants.TimeT) +// } + +// func TestGetValidPrices(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// }) + +// r := etp.GetValidPrices(constants.TimeT) +// require.Len(t, r, 1) +// require.Equal(t, constants.Price1, r[0]) +// } + +// func TestGetValidPrices_Empty(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// r := etp.GetValidPrices(constants.TimeT) +// require.Empty(t, r) +// } + +// func TestGetValidPrices_OldPricesEmpty(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }) + +// r := etp.GetValidPrices(constants.TimeTPlus1) +// require.Empty(t, r) +// } + +// func TestGetValidPrices_ValidAndOldPrices(t *testing.T) { +// etp := NewExchangeToPrice(0) + +// etp.UpdatePrices( +// []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price3_AfterTimeT, +// constants.Exchange3_Price4_AfterTimeT, +// }) + +// // Exchange 1's Price is before cutoff, so it's ignored +// r := etp.GetValidPrices(constants.TimeTPlus1) +// require.Len(t, r, 2) + +// expected := []uint64{constants.Price3, constants.Price4} +// assert.ElementsMatch(t, expected, r) +// } diff --git a/daemons/server/types/pricefeed/market_to_exchange_prices.go b/daemons/server/types/pricefeed/market_to_exchange_prices.go new file mode 100644 index 000000000..b9991ab0e --- /dev/null +++ b/daemons/server/types/pricefeed/market_to_exchange_prices.go @@ -0,0 +1,127 @@ +package types + +import ( + "sync" + "time" + + gometrics "github.com/armon/go-metrics" + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" + servertypes "github.com/tellor-io/layer/daemons/server/types" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/lib/metrics" +) + +// MarketToExchangePrices maintains price info for multiple markets. Each +// market can support prices from multiple exchange sources. Specifically, +// MarketToExchangePrices supports methods to update prices and to retrieve +// median prices. Methods are goroutine safe. +type MarketToExchangePrices struct { + sync.Mutex // lock + marketToExchangePrices map[uint32]*ExchangeToPrice // {k: market id, v: exchange prices} + // maxPriceAge is the maximum age of a price before it is considered too stale to be used. + // Prices older than this age will not be used to calculate the median price. + maxPriceAge time.Duration +} + +// NewMarketToExchangePrices creates a new MarketToExchangePrices. +func NewMarketToExchangePrices(maxPriceAge time.Duration) *MarketToExchangePrices { + return &MarketToExchangePrices{ + marketToExchangePrices: make(map[uint32]*ExchangeToPrice), + maxPriceAge: maxPriceAge, + } +} + +// UpdatePrices updates market prices given a list of price updates. Prices are +// only updated if the timestamp on the updates are greater than the timestamp +// on existing prices. +func (mte *MarketToExchangePrices) UpdatePrices( + updates []*servertypes.MarketPriceUpdate) { + mte.Lock() + defer mte.Unlock() + for _, marketPriceUpdate := range updates { + marketId := marketPriceUpdate.MarketId + exchangeToPrices, ok := mte.marketToExchangePrices[marketId] + if !ok { + exchangeToPrices = NewExchangeToPrice(marketId) + mte.marketToExchangePrices[marketId] = exchangeToPrices + } + exchangeToPrices.UpdatePrices(marketPriceUpdate.ExchangePrices) + } +} + +// GetValidMedianPrices returns median prices for multiple markets. +// Specifically, it returns a map where the key is the market ID and the value +// is the median price for the market. It only returns "valid" prices where +// 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, +) map[uint32]uint64 { + cutoffTime := readTime.Add(-mte.maxPriceAge) + marketIdToMedianPrice := make(map[uint32]uint64) + + mte.Lock() + defer mte.Unlock() + for _, marketParam := range marketParams { + marketId := marketParam.Id + exchangeToPrice, ok := mte.marketToExchangePrices[marketId] + if !ok { + // No market price info yet, skip this market. + telemetry.IncrCounterWithLabels( + []string{ + metrics.PricefeedServer, + metrics.NoMarketPrice, + metrics.Count, + }, + 1, + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(marketId), + }, + ) + continue + } + + // GetValidPriceForMarket filters prices based on cutoff time. + validPrices := exchangeToPrice.GetValidPrices(cutoffTime) + telemetry.SetGaugeWithLabels( + []string{ + metrics.PricefeedServer, + metrics.ValidPrices, + metrics.Count, + }, + float32(len(validPrices)), + []gometrics.Label{ + pricefeedmetrics.GetLabelForMarketId(marketId), + }, + ) + + // 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 + } + } + + return marketIdToMedianPrice +} diff --git a/daemons/server/types/pricefeed/market_to_exchange_prices_test.go b/daemons/server/types/pricefeed/market_to_exchange_prices_test.go new file mode 100644 index 000000000..20328848c --- /dev/null +++ b/daemons/server/types/pricefeed/market_to_exchange_prices_test.go @@ -0,0 +1,196 @@ +package types + +// import ( +// "testing" +// "time" + +// pricefeed_types "github.com/tellor-io/layer/daemons/pricefeed/types" + +// "github.com/stretchr/testify/require" +// "github.com/tellor-io/layer/daemons/pricefeed/api" +// "github.com/tellor-io/layer/testutil/constants" +// "github.com/tellor-io/layer/x/prices/types" +// ) + +// func TestNewMarketToExchangePrices_IsEmpty(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) + +// require.Empty(t, mte.marketToExchangePrices) +// } + +// func TestUpdatePrices_SingleUpdateSinglePrice(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) + +// mte.UpdatePrices( +// []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// }, +// }, +// }) + +// require.Len(t, mte.marketToExchangePrices, 1) +// _, ok := mte.marketToExchangePrices[constants.MarketId9] +// require.True(t, ok) +// } + +// func TestUpdatePrices_SingleUpdateMultiPrices(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) + +// mte.UpdatePrices( +// []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }, +// }, +// }) + +// require.Len(t, mte.marketToExchangePrices, 1) +// _, ok := mte.marketToExchangePrices[constants.MarketId9] +// require.True(t, ok) +// } + +// func TestUpdatePrices_MultiUpdatesMultiPrices(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) + +// mte.UpdatePrices( +// []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }, +// }, +// { +// MarketId: constants.MarketId8, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }, +// }, +// }) + +// require.Len(t, mte.marketToExchangePrices, 2) +// _, ok9 := mte.marketToExchangePrices[constants.MarketId9] +// require.True(t, ok9) +// _, ok8 := mte.marketToExchangePrices[constants.MarketId8] +// require.True(t, ok8) +// } + +// func TestUpdatePrices_MultiUpdatesMultiPricesRepeated(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) + +// mte.UpdatePrices( +// []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }, +// }, +// { +// MarketId: constants.MarketId9, // Repeated market +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange3_Price4_AfterTimeT, +// }, +// }, +// { +// MarketId: constants.MarketId8, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange2_Price2_TimeT, +// }, +// }, +// { +// MarketId: constants.MarketId8, // Repeated market +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price1_TimeT, +// constants.Exchange3_Price4_AfterTimeT, +// }, +// }, +// }) + +// require.Len(t, mte.marketToExchangePrices, 2) +// _, ok9 := mte.marketToExchangePrices[constants.MarketId9] +// require.True(t, ok9) +// _, ok8 := mte.marketToExchangePrices[constants.MarketId8] +// require.True(t, ok8) +// } + +// func TestGetValidMedianPrices_EmptyResult(t *testing.T) { +// tests := map[string]struct { +// updatePriceInput []*api.MarketPriceUpdate +// getPricesInputMarketParams []types.MarketParam +// getPricesInputTime time.Time +// }{ +// "No market specified": { +// updatePriceInput: constants.AtTimeTPriceUpdate, +// getPricesInputMarketParams: []types.MarketParam{}, // No market specified. +// getPricesInputTime: constants.TimeT, +// }, +// "No valid price timestamps": { +// updatePriceInput: constants.AtTimeTPriceUpdate, +// getPricesInputMarketParams: constants.AllMarketParamsMinExchanges2, +// // Updates @ timeT are invalid at this read time +// getPricesInputTime: constants.TimeTPlusThreshold.Add(time.Duration(1)), +// }, +// "Empty prices does not throw": { +// updatePriceInput: []*api.MarketPriceUpdate{ +// { +// MarketId: constants.MarketId9, +// ExchangePrices: []*api.ExchangePrice{ +// constants.Exchange1_Price3_BeforeTimeT, // Invalid time +// }, +// }, +// }, +// getPricesInputMarketParams: []types.MarketParam{ +// { +// Id: constants.MarketId9, +// MinExchanges: 0, // Set to 0 to trigger median calc error +// }, +// }, +// 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 { +// t.Run(name, func(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) +// mte.UpdatePrices(tc.updatePriceInput) +// r := mte.GetValidMedianPrices( +// tc.getPricesInputMarketParams, +// tc.getPricesInputTime, +// ) + +// require.Len(t, r, 0) // The result is empty. +// }) +// } +// } + +// func TestGetValidMedianPrices_MultiMarketSuccess(t *testing.T) { +// mte := NewMarketToExchangePrices(pricefeed_types.MaxPriceAge) + +// mte.UpdatePrices(constants.MixedTimePriceUpdate) + +// r := mte.GetValidMedianPrices(constants.AllMarketParamsMinExchanges2, constants.TimeT) + +// require.Len(t, r, 2) +// 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/daemons/server/types/stoppable.go b/daemons/server/types/stoppable.go new file mode 100644 index 000000000..fda69d34a --- /dev/null +++ b/daemons/server/types/stoppable.go @@ -0,0 +1,7 @@ +package types + +// Stoppable is an interface for a service that can be stopped. +// This is used to stop services registered with the health monitor. +type Stoppable interface { + Stop() +} diff --git a/daemons/types/file_handler.go b/daemons/types/file_handler.go new file mode 100644 index 000000000..4129256d4 --- /dev/null +++ b/daemons/types/file_handler.go @@ -0,0 +1,21 @@ +package types + +import ( + "os" +) + +// FileHandlerImpl is the struct that implements the `FileHandler` interface. +type FileHandlerImpl struct{} + +// Ensure the `FileHandlerImpl` struct is implemented at compile time. +var _ FileHandler = (*FileHandlerImpl)(nil) + +// FileHandler is an interface that encapsulates the os function `RemoveAll`. +type FileHandler interface { + RemoveAll(path string) error +} + +// RemoveAll wraps `os.RemoveAll` which removes everything at a given path. +func (r *FileHandlerImpl) RemoveAll(path string) error { + return os.RemoveAll(path) +} diff --git a/daemons/types/grpc_client.go b/daemons/types/grpc_client.go new file mode 100644 index 000000000..d2685e07f --- /dev/null +++ b/daemons/types/grpc_client.go @@ -0,0 +1,62 @@ +package types + +import ( + "context" + "net" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/tellor-io/layer/daemons/constants" +) + +// GrpcClientImpl is the struct that implements the `GrpcClient` interface. +type GrpcClientImpl struct{} + +// Ensure the `GrpcClient` interface is implemented at compile time. +var _ GrpcClient = (*GrpcClientImpl)(nil) + +// GrpcClient is an interface that encapsulates the `NewGrpcConnection` function and `CloseConnection`. +type GrpcClient interface { + NewGrpcConnection(ctx context.Context, socketAddress string) (*grpc.ClientConn, error) + NewTcpConnection(ctx context.Context, endpoint string) (*grpc.ClientConn, error) + CloseConnection(grpcConn *grpc.ClientConn) error +} + +// NewGrpcConnection calls `grpc.Dial` with custom parameters to create a secure connection +// with context that blocks until the underlying connection is up. +func (g *GrpcClientImpl) NewGrpcConnection( + ctx context.Context, + socketAddress string, +) (*grpc.ClientConn, error) { + return grpc.DialContext( + ctx, + socketAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + // https://github.com/grpc/grpc-go/blob/master/dialoptions.go#L264 + grpc.WithBlock(), + grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { + // Create a custom `net.Dialer` in order to specify `unix` as the desired network. + var dialer net.Dialer + return dialer.DialContext(ctx, constants.UnixProtocol, addr) + }), + ) +} + +// NewTcpConnection calls `grpc.Dial` to create an insecure tcp connection. +func (g *GrpcClientImpl) NewTcpConnection( + ctx context.Context, + endpoint string, +) (*grpc.ClientConn, error) { + return grpc.DialContext( + ctx, + endpoint, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) +} + +// CloseConnection calls `grpc.ClientConn.Close()` to close a grpc connection. +func (g *GrpcClientImpl) CloseConnection(grpcConn *grpc.ClientConn) error { + return grpcConn.Close() +} diff --git a/daemons/types/grpc_server.go b/daemons/types/grpc_server.go new file mode 100644 index 000000000..9d743c3fb --- /dev/null +++ b/daemons/types/grpc_server.go @@ -0,0 +1,17 @@ +package types + +import ( + "net" + + "google.golang.org/grpc" +) + +// Ensure the `GrpcServer` interface is implemented at compile time. +var _ GrpcServer = (*grpc.Server)(nil) + +// GrpcServer is an interface that encapsulates a `Grpc.Server` object. +type GrpcServer interface { + Serve(lis net.Listener) error + Stop() + RegisterService(sd *grpc.ServiceDesc, ss interface{}) +} diff --git a/daemons/types/health_checkable.go b/daemons/types/health_checkable.go new file mode 100644 index 000000000..85566805f --- /dev/null +++ b/daemons/types/health_checkable.go @@ -0,0 +1,156 @@ +package types + +import ( + "fmt" + "sync" + "time" + + "github.com/cometbft/cometbft/libs/log" + libtime "github.com/tellor-io/layer/lib/time" +) + +const ( + MaxAcceptableUpdateDelay = 5 * time.Minute +) + +// HealthCheckable is a common interface for services that can be health checked. +// +// Instances of this type are thread-safe. +type HealthCheckable interface { + // HealthCheck returns an error if a service is unhealthy. If the service is healthy, this method returns nil. + HealthCheck() (err error) + // ReportFailure records a failed update. + ReportFailure(err error) + // ReportSuccess records a successful update. + ReportSuccess() + // ServiceName returns the name of the service being monitored. This name is expected to be unique. + ServiceName() string +} + +// timestampWithError couples a timestamp and error to make it easier to update them in tandem. +type timestampWithError struct { + timestamp time.Time + err error +} + +func (u *timestampWithError) Update(timestamp time.Time, err error) { + u.timestamp = timestamp + u.err = err +} + +func (u *timestampWithError) Timestamp() time.Time { + return u.timestamp +} + +func (u *timestampWithError) Error() error { + return u.err +} + +// timeBoundedHealthCheckable implements the HealthCheckable interface by tracking the timestamps of the last successful +// and failed updates. +// If any of the following occurs, then the service should be considered unhealthy: +// - no update has occurred +// - the most recent update failed, or +// - the daemon has not seen a successful update within `MaxAcceptableUpdateDelay`. +// +// This object is thread-safe. +type timeBoundedHealthCheckable struct { + sync.Mutex + + // lastSuccessfulUpdate is the timestamp of the last successful update. + lastSuccessfulUpdate time.Time + // lastFailedUpdate is the timestamp, error pair of the last failed update. + lastFailedUpdate timestampWithError + + // timeProvider is the time provider used to determine the current time. This is used for timestamping + // creation and checking for update staleness during HealthCheck. + timeProvider libtime.TimeProvider + + // logger is the logger used to log errors. + logger log.Logger + + // serviceName is the name of the service being monitored. This field is read-only and not synchronized. + serviceName string +} + +// NewTimeBoundedHealthCheckable creates a new HealthCheckable instance. +func NewTimeBoundedHealthCheckable( + serviceName string, + timeProvider libtime.TimeProvider, + logger log.Logger, +) HealthCheckable { + hc := &timeBoundedHealthCheckable{ + timeProvider: timeProvider, + logger: logger, + serviceName: serviceName, + } + // Initialize the timeBoudnedHealthCheckable to an unhealthy state by reporting an error. + hc.ReportFailure(fmt.Errorf("%v is initializing", serviceName)) + return hc +} + +// ServiceName returns the name of the service being monitored. +func (hc *timeBoundedHealthCheckable) ServiceName() string { + return hc.serviceName +} + +// ReportSuccess records a successful update. This method is thread-safe. +func (h *timeBoundedHealthCheckable) ReportSuccess() { + h.Lock() + defer h.Unlock() + + h.lastSuccessfulUpdate = h.timeProvider.Now() +} + +// ReportFailure records a failed update. This method is thread-safe. +func (h *timeBoundedHealthCheckable) ReportFailure(err error) { + h.Lock() + defer h.Unlock() + h.lastFailedUpdate.Update(h.timeProvider.Now(), err) +} + +// HealthCheck returns an error if a service is unhealthy. +// The service is unhealthy if any of the following are true: +// - no successful update has occurred +// - the most recent update failed, or +// - the daemon has not seen a successful update in at least 5 minutes, +// Note: since the timeBoundedHealthCheckable is not exposed and can only be created via +// NewTimeBoundedHealthCheckable, we expect that the lastFailedUpdate is never a zero value. +// This method is thread-safe. +func (h *timeBoundedHealthCheckable) HealthCheck() error { + h.Lock() + defer h.Unlock() + + if h.lastSuccessfulUpdate.IsZero() { + return fmt.Errorf( + "no successful update has occurred; last failed update occurred at %v with error '%w'", + h.lastFailedUpdate.Timestamp(), + h.lastFailedUpdate.Error(), + ) + } + + if h.lastFailedUpdate.Timestamp().After(h.lastSuccessfulUpdate) { + return fmt.Errorf( + "last update failed at %v with error: '%w', most recent successful update occurred at %v", + h.lastFailedUpdate.Timestamp(), + h.lastFailedUpdate.Error(), + h.lastSuccessfulUpdate, + ) + } + + // If the last successful update was more than 5 minutes ago, log the specific error. + if h.timeProvider.Now().Sub(h.lastSuccessfulUpdate) > MaxAcceptableUpdateDelay { + h.logger.Error( + fmt.Sprintf( + "last successful update occurred at %v, which is more than %v ago. "+ + "Last failure occurred at %v with error '%v'", + h.lastSuccessfulUpdate, + MaxAcceptableUpdateDelay, + h.lastFailedUpdate.Timestamp(), + h.lastFailedUpdate.Error(), + ), + ) + } + + return nil +} diff --git a/daemons/types/request_handler.go b/daemons/types/request_handler.go new file mode 100644 index 000000000..cc023426a --- /dev/null +++ b/daemons/types/request_handler.go @@ -0,0 +1,33 @@ +package types + +import ( + "context" + "net/http" +) + +// RequestHandlerImpl is the struct that implements the `RequestHandler` interface. +type RequestHandlerImpl struct { + client *http.Client +} + +// RequestHandler is an interface that handles making HTTP requests. +type RequestHandler interface { + Get(ctx context.Context, url string) (*http.Response, error) +} + +// NewRequestHandlerImpl creates a new RequestHandlerImpl. It manages making HTTP requests. +func NewRequestHandlerImpl(client *http.Client) *RequestHandlerImpl { + return &RequestHandlerImpl{ + client: client, + } +} + +// Get wraps `http.Get` which makes an HTTP GET request to a URL and returns a response. +func (r *RequestHandlerImpl) Get(ctx context.Context, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + return r.client.Do(req) +} diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 5c82275ae..80b346c1e 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46437,6 +46437,741 @@ paths: } tags: - Query + /tellor-io/layer/dispute/params: + get: + summary: Parameters queries the parameters of the module. + operationId: LayerDisputeParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + tags: + - Query + /layer/oracle/get_reportsby_qid/{queryId}: + get: + summary: Queries a list of GetReportsbyQid items. + operationId: LayerOracleGetReportsbyQid + responses: + '200': + description: A successful response. + schema: + type: object + properties: + reports: + type: object + properties: + microReports: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryType: + type: string + queryId: + type: string + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: queryId + in: path + required: true + type: string + tags: + - Query + /layer/oracle/get_reportsby_reporter/{reporter}: + get: + operationId: LayerOracleGetReportsbyReporter + responses: + '200': + description: A successful response. + schema: + type: object + properties: + microReports: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryType: + type: string + queryId: + type: string + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: reporter + in: path + required: true + type: string + tags: + - Query + /layer/oracle/get_reportsby_reporter_qid/{reporter}/{queryId}: + get: + operationId: LayerOracleGetReportsbyReporterQid + responses: + '200': + description: A successful response. + schema: + type: object + properties: + reports: + type: object + properties: + microReports: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryType: + type: string + queryId: + type: string + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: reporter + in: path + required: true + type: string + - name: queryId + in: path + required: true + type: string + tags: + - Query + /layer/oracle/params: + get: + summary: Parameters queries the parameters of the module. + operationId: LayerOracleParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + properties: + cycle_list: + type: array + items: + type: string + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + tags: + - Query + /tellor-io/layer/oracle/GetDataBefore/{queryId}/{timestamp}: + get: + summary: Queries a list of GetAggregatedReport items. + operationId: LayerOracleGetDataBefore + responses: + '200': + description: A successful response. + schema: + type: object + properties: + report: + type: object + properties: + queryId: + type: string + aggregateValue: + type: string + aggregateReporter: + type: string + reporterPower: + type: string + format: int64 + standardDeviation: + type: number + format: double + reporters: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + flagged: + type: boolean + nonce: + type: string + format: int64 + aggregateReportIndex: + type: string + format: int64 + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: queryId + in: path + required: true + type: string + - name: timestamp + in: path + required: true + type: string + format: int64 + tags: + - Query + /tellor-io/layer/oracle/get_aggregated_report/{queryId}: + get: + summary: Queries a list of GetAggregatedReport items. + operationId: LayerOracleGetAggregatedReport + responses: + '200': + description: A successful response. + schema: + type: object + properties: + report: + type: object + properties: + queryId: + type: string + aggregateValue: + type: string + aggregateReporter: + type: string + reporterPower: + type: string + format: int64 + standardDeviation: + type: number + format: double + reporters: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + flagged: + type: boolean + nonce: + type: string + format: int64 + aggregateReportIndex: + type: string + format: int64 + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: queryId + in: path + required: true + type: string + tags: + - Query + /tellor-io/layer/oracle/get_current_tip/{queryData}: + get: + summary: Queries a list of GetCurrentTip items. + operationId: LayerOracleGetCurrentTip + responses: + '200': + description: A successful response. + schema: + type: object + properties: + tips: + type: object + properties: + queryData: + type: string + amount: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the + custom method + + signatures required by gogoproto. + totalTips: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the + custom method + + signatures required by gogoproto. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: queryData + in: path + required: true + type: string + tags: + - Query + /tellor-io/layer/oracle/get_median_values: + get: + summary: Queries a list of GetMedianValues items. + operationId: LayerOracleGetMedianValues + responses: + '200': + description: A successful response. + schema: + type: object + properties: + medianValues: + type: array + items: + type: object + properties: + marketId: + type: integer + format: int64 + price: + type: string + format: uint64 + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + tags: + - Query + /tellor-io/layer/oracle/get_time_based_rewards: + get: + summary: Queries a list of GetTimeBasedRewards items. + operationId: LayerOracleGetTimeBasedRewards + responses: + '200': + description: A successful response. + schema: + type: object + properties: + reward: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + tags: + - Query + /tellor-io/layer/oracle/get_user_tip_total/{tipper}/{queryData}: + get: + summary: Queries a list of GetUserTipTotal items. + operationId: LayerOracleGetUserTipTotal + responses: + '200': + description: A successful response. + schema: + type: object + properties: + totalTips: + type: object + properties: + address: + type: string + total: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the + custom method + + signatures required by gogoproto. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: tipper + in: path + required: true + type: string + - name: queryData + in: path + required: true + type: string + tags: + - Query + /layer/registry/get_data_spec/{queryType}: + get: + summary: Queries a list of GetDataSpec items. + operationId: LayerRegistryGetDataSpec + responses: + '200': + description: A successful response. + schema: + type: object + properties: + spec: + type: object + properties: + documentHash: + type: string + valueType: + type: string + aggregationMethod: + type: string + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: queryType + in: path + required: true + type: string + tags: + - Query + /layer/registry/get_query_data/{queryId}: + get: + summary: Queries a list of GetQueryData items. + operationId: LayerRegistryGetQueryData + responses: + '200': + description: A successful response. + schema: + type: object + properties: + queryData: + type: string + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: queryId + in: path + required: true + type: string + tags: + - Query + /layer/registry/params: + get: + summary: Parameters queries the parameters of the module. + operationId: LayerRegistryParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + description: >- + QueryParamsResponse is response type for the Query/Params RPC + method. + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + tags: + - Query definitions: cosmos.auth.v1beta1.AddressBytesToStringResponse: type: object @@ -74750,72 +75485,370 @@ definitions: Example 4: Pack and unpack a message in Go - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + client_id: + type: string + title: client ID associated with the consensus state + proof: + type: string + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping + + RevisionNumber the same. However some consensus algorithms may choose + to + + reset the height in certain conditions e.g. hard forks, state-machine + + breaking changes In these cases, the RevisionNumber is incremented so + that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + title: |- + QueryConnectionConsensusStateResponse is the response type for the + Query/ConnectionConsensusState RPC method + ibc.core.connection.v1.QueryConnectionParamsResponse: + type: object + properties: + params: + description: params defines the parameters of the module. + type: object + properties: + max_expected_time_per_block: + type: string + format: uint64 + description: >- + maximum expected time per block (in nanoseconds), used to enforce + block delay. This parameter should reflect the + + largest amount of time that the chain might reasonably take to + produce the next block under normal operating + + conditions. A safe choice is 3-5x the expected time per block. + description: >- + QueryConnectionParamsResponse is the response type for the + Query/ConnectionParams RPC method. + ibc.core.connection.v1.QueryConnectionResponse: + type: object + properties: + connection: + title: connection associated with the request identifier + type: object + properties: + client_id: + type: string + description: client associated with this connection. + versions: + type: array + items: + type: object + properties: + identifier: + type: string + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: >- + Version defines the versioning scheme used to negotiate the IBC + verison in + + the connection handshake. + description: >- + IBC version which can be utilised to determine encodings or + protocols for + + channels or packets utilising this connection. + state: + description: current state of the connection end. + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + counterparty: + description: counterparty chain associated with this connection. + type: object + properties: + client_id: + type: string + description: >- + identifies the client on the counterparty chain associated + with a given + + connection. + connection_id: + type: string + description: >- + identifies the connection end on the counterparty chain + associated with a + + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. + + The constructed key from the Path and the key will be + append(Path.KeyPath, + + append(Path.KeyPrefix, key...)) + delay_period: + type: string + format: uint64 + description: >- + delay period that must pass before a consensus state can be used + for + + packet-verification NOTE: delay period logic is only implemented + by some - The pack methods provided by protobuf library will by default use + clients. + description: >- + ConnectionEnd defines a stateful object on a chain connected to + another - 'type.googleapis.com/full.type.name' as the type URL and the unpack + separate one. - methods only use the fully qualified type name after the last '/' + NOTE: there must only be 2 defined ConnectionEnds to establish - in the type URL, for example "foo.bar.com/x/y.z" will yield type + a connection between two chains. + proof: + type: string + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping - name "y.z". + RevisionNumber the same. However some consensus algorithms may choose + to + reset the height in certain conditions e.g. hard forks, state-machine + breaking changes In these cases, the RevisionNumber is incremented so + that - JSON + height continues to be monitonically increasing even as the + RevisionHeight - ==== + gets reset + description: >- + QueryConnectionResponse is the response type for the Query/Connection RPC - The JSON representation of an `Any` value uses the regular + method. Besides the connection end, it includes a proof and the height + from - representation of the deserialized, embedded message, with an + which the proof was retrieved. + ibc.core.connection.v1.QueryConnectionsResponse: + type: object + properties: + connections: + type: array + items: + type: object + properties: + id: + type: string + description: connection identifier. + client_id: + type: string + description: client associated with this connection. + versions: + type: array + items: + type: object + properties: + identifier: + type: string + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: >- + Version defines the versioning scheme used to negotiate the + IBC verison in - additional field `@type` which contains the type URL. Example: + the connection handshake. + title: >- + IBC version which can be utilised to determine encodings or + protocols for - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + channels or packets utilising this connection + state: + description: current state of the connection end. + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + counterparty: + description: counterparty chain associated with this connection. + type: object + properties: + client_id: + type: string + description: >- + identifies the client on the counterparty chain associated + with a given - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + connection. + connection_id: + type: string + description: >- + identifies the connection end on the counterparty chain + associated with a - If the embedded message type is well-known and has a custom JSON + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. - representation, that representation will be embedded adding a field + The constructed key from the Path and the key will be + append(Path.KeyPath, - `value` which holds the custom JSON in addition to the `@type` + append(Path.KeyPrefix, key...)) + delay_period: + type: string + format: uint64 + description: delay period associated with this connection. + description: |- + IdentifiedConnection defines a connection with additional connection + identifier field. + description: list of stored connections of the chain. + pagination: + title: pagination response + type: object + properties: + next_key: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: + type: string + format: uint64 + title: >- + total is total number of results available if + PageRequest.count_total - field. Example (for message [google.protobuf.Duration][]): + was set, its value is undefined otherwise + description: |- + PageResponse is to be embedded in gRPC response messages where the + corresponding request message has used PageRequest. - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - client_id: - type: string - title: client ID associated with the consensus state - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } + height: + title: query block height type: object properties: revision_number: @@ -74842,337 +75875,489 @@ definitions: RevisionHeight gets reset - title: |- - QueryConnectionConsensusStateResponse is the response type for the - Query/ConnectionConsensusState RPC method - ibc.core.connection.v1.QueryConnectionParamsResponse: + description: >- + QueryConnectionsResponse is the response type for the Query/Connections + RPC + + method. + ibc.core.connection.v1.State: + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + description: |- + State defines if a connection is in one of the following states: + INIT, TRYOPEN, OPEN or UNINITIALIZED. + + - STATE_UNINITIALIZED_UNSPECIFIED: Default State + - STATE_INIT: A connection end has just started the opening handshake. + - STATE_TRYOPEN: A connection end has acknowledged the handshake step on the counterparty + chain. + - STATE_OPEN: A connection end has completed the handshake. + ibc.core.connection.v1.Version: + type: object + properties: + identifier: + type: string + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: |- + Version defines the versioning scheme used to negotiate the IBC verison in + the connection handshake. + layer.dispute.DisputeCategory: + type: string + enum: + - DISPUTE_CATEGORY_UNSPECIFIED + - DISPUTE_CATEGORY_WARNING + - DISPUTE_CATEGORY_MINOR + - DISPUTE_CATEGORY_MAJOR + default: DISPUTE_CATEGORY_UNSPECIFIED + description: |2- + - DISPUTE_CATEGORY_UNSPECIFIED: UNSPECIFIED defines an invalid dispute category. + - DISPUTE_CATEGORY_WARNING: WARNING defines a 1 percent slashing. + - DISPUTE_CATEGORY_MINOR: MINOR defines a 5 percent slashing. + - DISPUTE_CATEGORY_MAJOR: MAJOR defines a 100 percent slashing. + layer.dispute.MicroReport: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryId: + type: string + value: + type: string + timestamp: + type: string + format: int64 + layer.dispute.MsgAddFeeToDisputeResponse: + type: object + layer.dispute.MsgProposeDisputeResponse: + type: object + layer.dispute.MsgVoteResponse: + type: object + layer.dispute.Params: + type: object + description: Params defines the parameters for the module. + layer.dispute.QueryParamsResponse: type: object properties: params: - description: params defines the parameters of the module. + description: params holds all the parameters of this module. type: object - properties: - max_expected_time_per_block: - type: string - format: uint64 - description: >- - maximum expected time per block (in nanoseconds), used to enforce - block delay. This parameter should reflect the - - largest amount of time that the chain might reasonably take to - produce the next block under normal operating - - conditions. A safe choice is 3-5x the expected time per block. - description: >- - QueryConnectionParamsResponse is the response type for the - Query/ConnectionParams RPC method. - ibc.core.connection.v1.QueryConnectionResponse: + description: QueryParamsResponse is response type for the Query/Params RPC method. + layer.dispute.VoteEnum: + type: string + enum: + - VOTE_INVALID + - VOTE_SUPPORT + - VOTE_AGAINST + default: VOTE_INVALID + layer.oracle.Aggregate: type: object properties: - connection: - title: connection associated with the request identifier + queryId: + type: string + aggregateValue: + type: string + aggregateReporter: + type: string + reporterPower: + type: string + format: int64 + standardDeviation: + type: number + format: double + reporters: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + flagged: + type: boolean + nonce: + type: string + format: int64 + aggregateReportIndex: + type: string + format: int64 + layer.oracle.AggregateReporter: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + layer.oracle.MedianValues: + type: object + properties: + marketId: + type: integer + format: int64 + price: + type: string + format: uint64 + layer.oracle.MicroReport: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryType: + type: string + queryId: + type: string + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + layer.oracle.MsgCommitReportResponse: + type: object + layer.oracle.MsgSubmitValueResponse: + type: object + layer.oracle.MsgTipResponse: + type: object + layer.oracle.MsgUpdateParamsResponse: + type: object + layer.oracle.Params: + type: object + properties: + cycle_list: + type: array + items: + type: string + description: Params defines the parameters for the module. + layer.oracle.QueryGetAggregatedReportResponse: + type: object + properties: + report: type: object properties: - client_id: + queryId: type: string - description: client associated with this connection. - versions: + aggregateValue: + type: string + aggregateReporter: + type: string + reporterPower: + type: string + format: int64 + standardDeviation: + type: number + format: double + reporters: type: array items: type: object properties: - identifier: + reporter: type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: >- - Version defines the versioning scheme used to negotiate the IBC - verison in - - the connection handshake. - description: >- - IBC version which can be utilised to determine encodings or - protocols for - - channels or packets utilising this connection. - state: - description: current state of the connection end. + power: + type: string + format: int64 + flagged: + type: boolean + nonce: type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - counterparty: - description: counterparty chain associated with this connection. + format: int64 + aggregateReportIndex: + type: string + format: int64 + layer.oracle.QueryGetCurrentTipResponse: + type: object + properties: + tips: + type: object + properties: + queryData: + type: string + amount: type: object properties: - client_id: + denom: type: string - description: >- - identifies the client on the counterparty chain associated - with a given - - connection. - connection_id: + amount: type: string - description: >- - identifies the connection end on the counterparty chain - associated with a + description: >- + Coin defines a token with a denomination and an amount. - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. - The constructed key from the Path and the key will be - append(Path.KeyPath, + NOTE: The amount field is an Int which implements the custom + method - append(Path.KeyPrefix, key...)) - delay_period: - type: string - format: uint64 + signatures required by gogoproto. + totalTips: + type: object + properties: + denom: + type: string + amount: + type: string description: >- - delay period that must pass before a consensus state can be used - for - - packet-verification NOTE: delay period logic is only implemented - by some - - clients. - description: >- - ConnectionEnd defines a stateful object on a chain connected to - another + Coin defines a token with a denomination and an amount. - separate one. - NOTE: there must only be 2 defined ConnectionEnds to establish + NOTE: The amount field is an Int which implements the custom + method - a connection between two chains. - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved + signatures required by gogoproto. + layer.oracle.QueryGetMedianValuesResponse: + type: object + properties: + medianValues: + type: array + items: + type: object + properties: + marketId: + type: integer + format: int64 + price: + type: string + format: uint64 + layer.oracle.QueryGetReportsbyQidResponse: + type: object + properties: + reports: type: object properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: + microReports: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryType: + type: string + queryId: + type: string + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + layer.oracle.QueryGetReportsbyReporterResponse: + type: object + properties: + microReports: + type: array + items: + type: object + properties: + reporter: + type: string + power: + type: string + format: int64 + queryType: + type: string + queryId: + type: string + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + layer.oracle.QueryGetTimeBasedRewardsResponse: + type: object + properties: + reward: + type: object + properties: + denom: type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that + amount: + type: string + description: |- + Coin defines a token with a denomination and an amount. - height continues to be monitonically increasing even as the - RevisionHeight + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + layer.oracle.QueryGetUserTipTotalResponse: + type: object + properties: + totalTips: + type: object + properties: + address: + type: string + total: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. - gets reset - description: >- - QueryConnectionResponse is the response type for the Query/Connection RPC - method. Besides the connection end, it includes a proof and the height - from + NOTE: The amount field is an Int which implements the custom + method - which the proof was retrieved. - ibc.core.connection.v1.QueryConnectionsResponse: + signatures required by gogoproto. + layer.oracle.QueryParamsResponse: type: object properties: - connections: + params: + description: params holds all the parameters of this module. + type: object + properties: + cycle_list: + type: array + items: + type: string + description: QueryParamsResponse is response type for the Query/Params RPC method. + layer.oracle.Reports: + type: object + properties: + microReports: type: array items: type: object properties: - id: + reporter: type: string - description: connection identifier. - client_id: + power: type: string - description: client associated with this connection. - versions: - type: array - items: - type: object - properties: - identifier: - type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: >- - Version defines the versioning scheme used to negotiate the - IBC verison in - - the connection handshake. - title: >- - IBC version which can be utilised to determine encodings or - protocols for - - channels or packets utilising this connection - state: - description: current state of the connection end. + format: int64 + queryType: type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - counterparty: - description: counterparty chain associated with this connection. - type: object - properties: - client_id: - type: string - description: >- - identifies the client on the counterparty chain associated - with a given - - connection. - connection_id: - type: string - description: >- - identifies the connection end on the counterparty chain - associated with a - - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. - - The constructed key from the Path and the key will be - append(Path.KeyPath, - - append(Path.KeyPrefix, key...)) - delay_period: + queryId: type: string - format: uint64 - description: delay period associated with this connection. - description: |- - IdentifiedConnection defines a connection with additional connection - identifier field. - description: list of stored connections of the chain. - pagination: - title: pagination response + aggregateMethod: + type: string + value: + type: string + blockNumber: + type: string + format: int64 + timestamp: + type: string + format: date-time + layer.oracle.Tips: + type: object + properties: + queryData: + type: string + amount: type: object properties: - next_key: + denom: type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: + amount: type: string - format: uint64 - title: >- - total is total number of results available if - PageRequest.count_total - - was set, its value is undefined otherwise description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. + Coin defines a token with a denomination and an amount. - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - height: - title: query block height + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + totalTips: type: object properties: - revision_number: + denom: type: string - format: uint64 - title: the revision that the client is currently on - revision_height: + amount: type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - description: >- - QueryConnectionsResponse is the response type for the Query/Connections - RPC + description: |- + Coin defines a token with a denomination and an amount. - method. - ibc.core.connection.v1.State: - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - description: |- - State defines if a connection is in one of the following states: - INIT, TRYOPEN, OPEN or UNINITIALIZED. + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + layer.oracle.UserTipTotal: + type: object + properties: + address: + type: string + total: + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + Coin defines a token with a denomination and an amount. - - STATE_UNINITIALIZED_UNSPECIFIED: Default State - - STATE_INIT: A connection end has just started the opening handshake. - - STATE_TRYOPEN: A connection end has acknowledged the handshake step on the counterparty - chain. - - STATE_OPEN: A connection end has completed the handshake. - ibc.core.connection.v1.Version: + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + layer.registry.DataSpec: type: object properties: - identifier: + documentHash: type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: |- - Version defines the versioning scheme used to negotiate the IBC verison in - the connection handshake. + valueType: + type: string + aggregationMethod: + type: string + layer.registry.MsgRegisterQueryResponse: + type: object + properties: + queryId: + type: string + layer.registry.MsgRegisterSpecResponse: + type: object + layer.registry.Params: + type: object + description: Params defines the parameters for the module. + layer.registry.QueryGetDataSpecResponse: + type: object + properties: + spec: + type: object + properties: + documentHash: + type: string + valueType: + type: string + aggregationMethod: + type: string + layer.registry.QueryGetQueryDataResponse: + type: object + properties: + queryData: + type: string + layer.registry.QueryParamsResponse: + type: object + properties: + params: + description: params holds all the parameters of this module. + type: object + description: QueryParamsResponse is response type for the Query/Params RPC method. diff --git a/go.mod b/go.mod index 1ce4a0ab3..25acc54b4 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,9 @@ require ( cosmossdk.io/core v0.5.1 cosmossdk.io/depinject v1.0.0-alpha.4 cosmossdk.io/errors v1.0.0 + cosmossdk.io/log v1.2.1 cosmossdk.io/math v1.1.2 + github.com/armon/go-metrics v0.4.1 github.com/cometbft/cometbft v0.37.2 github.com/cometbft/cometbft-db v0.7.0 github.com/cosmos/cosmos-proto v1.0.0-beta.2 @@ -15,17 +17,22 @@ require ( github.com/cosmos/gogoproto v1.4.10 github.com/cosmos/ibc-go/v7 v7.1.0 github.com/ethereum/go-ethereum v1.10.22 + github.com/go-playground/validator/v10 v10.12.0 github.com/golang/protobuf v1.5.3 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 + github.com/pelletier/go-toml v1.9.5 + github.com/shopspring/decimal v1.3.1 github.com/spf13/cast v1.5.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 google.golang.org/grpc v1.56.2 google.golang.org/protobuf v1.31.0 + gopkg.in/typ.v4 v4.3.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -35,13 +42,11 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.0 // indirect cloud.google.com/go/storage v1.30.1 // indirect - cosmossdk.io/log v1.2.1 // indirect cosmossdk.io/tools/rosetta v0.2.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect - github.com/armon/go-metrics v0.4.1 // indirect github.com/aws/aws-sdk-go v1.44.203 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect @@ -82,7 +87,8 @@ require ( github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-playground/validator/v10 v10.12.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -120,6 +126,7 @@ require ( github.com/klauspost/compress v1.16.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.2 // indirect github.com/lib/pq v1.10.7 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -162,7 +169,6 @@ require ( go.etcd.io/bbolt v1.3.7 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.11.0 // indirect - golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.3.0 // indirect @@ -182,4 +188,7 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 +replace ( + github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 +) diff --git a/go.sum b/go.sum index 7881623af..c5bad47ae 100644 --- a/go.sum +++ b/go.sum @@ -436,10 +436,13 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= @@ -459,12 +462,6 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= @@ -685,8 +682,6 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -708,6 +703,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= +github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= @@ -808,6 +804,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= @@ -864,6 +862,8 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -879,11 +879,14 @@ github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiS github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -934,6 +937,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= @@ -1279,10 +1283,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1434,6 +1436,7 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -1603,6 +1606,8 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/typ.v4 v4.3.0 h1:PEQtVIdhjOo4sOLnqpuEYrfSsul+a85EBGHS7tDJFuU= +gopkg.in/typ.v4 v4.3.0/go.mod h1:wolXe8DlewxRCjA7SOiT3zjrZ0eQJZcr8cmV6bQWJUM= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/big_math.go b/lib/big_math.go new file mode 100644 index 000000000..928d9ed2e --- /dev/null +++ b/lib/big_math.go @@ -0,0 +1,55 @@ +package lib + +import ( + "fmt" + "math/big" +) + +// bigPow10Memo is a cache of the most common exponent value requests. Since bigPow10Memo will be +// accessed from different go-routines, the map should only ever be read from or collision +// could occur. +var bigPow10Memo = warmCache() + +// BigPow10 returns the result of `10^exponent`. Caches all calculated values and +// re-uses cached values in any following calls to BigPow10. +func BigPow10(exponent uint64) *big.Int { + result := bigPow10Helper(exponent) + // Copy the result, such that no values can be modified by reference in the + // `bigPow10Memo` cache. + copy := new(big.Int).Set(result) + return copy +} + +func bigPow10Helper(exponent uint64) *big.Int { + m, ok := bigPow10Memo[exponent] + if ok { + return m + } + + // Subdivide the exponent and recursively calculate each result, then multiply + // both results together (given that `10^exponent = 10^(exponent / 2) * + // 10^(exponent - (exponent / 2))`. + e1 := exponent / 2 + e2 := exponent - e1 + return new(big.Int).Mul(bigPow10Helper(e1), bigPow10Helper(e2)) +} + +// warmCache is used to populate `bigPow10Memo` with the most common exponent requests. Since, +// none of the exponents should ever be invalid - panic immediately if an exponent is cannot be +// parsed. +func warmCache() map[uint64]*big.Int { + exponentString := "1" + bigExponentValues := make(map[uint64]*big.Int, 100) + for i := 0; i < 100; i++ { + bigValue, ok := new(big.Int).SetString(exponentString, 0) + + if !ok { + panic(fmt.Sprintf("Failed to get big from string for exponent memo: %v", exponentString)) + } + + bigExponentValues[uint64(i)] = bigValue + exponentString = exponentString + "0" + } + + return bigExponentValues +} diff --git a/lib/collections.go b/lib/collections.go new file mode 100644 index 000000000..1a2dcd58f --- /dev/null +++ b/lib/collections.go @@ -0,0 +1,18 @@ +package lib + +import ( + "sort" +) + +// GetSortedKeys returns the keys of the map in sorted order. +func GetSortedKeys[R interface { + ~[]K + sort.Interface +}, K comparable, V any](m map[K]V) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Sort(R(keys)) + return keys +} diff --git a/lib/constants.go b/lib/constants.go new file mode 100644 index 000000000..db583b12f --- /dev/null +++ b/lib/constants.go @@ -0,0 +1,16 @@ +package lib + +import ( + "math" + "math/big" +) + +// BigFloatMaxUint64 returns a `big.Float` that is set to MaxUint64. +func BigFloatMaxUint64() *big.Float { + return new(big.Float).SetUint64(math.MaxUint64) +} + +// BigFloat0 returns a `big.Float` that is set to 0. +func BigFloat0() *big.Float { + return big.NewFloat(0) +} diff --git a/lib/convert.go b/lib/convert.go new file mode 100644 index 000000000..724e8a994 --- /dev/null +++ b/lib/convert.go @@ -0,0 +1,55 @@ +package lib + +import ( + "errors" + "fmt" + "math/big" +) + +// ConvertBigFloatSliceToUint64Slice attempts to convert all values in a slice +// from big Float to uint64 and return an error if any conversions fail. Note: during conversion, +// will always round down. +func ConvertBigFloatSliceToUint64Slice(values []*big.Float) ([]uint64, error) { + convertedValues := make([]uint64, 0, len(values)) + for _, value := range values { + uint64Value, err := ConvertBigFloatToUint64(value) + if err != nil { + return nil, err + } + + convertedValues = append(convertedValues, uint64Value) + } + + return convertedValues, nil +} + +// ConvertBigFloatToUint64 attempts to convert a big Float into a uint64 and returns an error +// if the conversion would fail. Note: during conversion, will always round down. +func ConvertBigFloatToUint64(value *big.Float) (uint64, error) { + if value.Cmp(BigFloatMaxUint64()) == 1 { + return 0, errors.New("value overflows uint64") + } + + if value.Cmp(BigFloat0()) == -1 { + return 0, errors.New("value underflows uint64") + } + + uint64Val, _ := value.Uint64() + return uint64Val, nil +} + +// ConvertStringSliceToBigFloatSlice attempts to convert all values in a slice +// from string to big Float and return an error if any conversions fail. +func ConvertStringSliceToBigFloatSlice(values []string) ([]*big.Float, error) { + convertedValues := make([]*big.Float, 0, len(values)) + for _, value := range values { + bigValue, success := new(big.Float).SetString(value) + if !success { + return nil, fmt.Errorf("invalid, value is not a number: %v", value) + } + + convertedValues = append(convertedValues, bigValue) + } + + return convertedValues, nil +} diff --git a/lib/math.go b/lib/math.go new file mode 100644 index 000000000..a53cd3e63 --- /dev/null +++ b/lib/math.go @@ -0,0 +1,62 @@ +package lib + +import ( + "errors" + "sort" + + "golang.org/x/exp/constraints" +) + +// Median is a generic median calculator. +// If the input has an even number of elements, then the average of the two middle numbers is rounded away from zero. +func Median[V uint64 | uint32 | int64 | int32](input []V) (V, error) { + l := len(input) + if l == 0 { + return 0, errors.New("input cannot be empty") + } + + inputCopy := make([]V, l) + copy(inputCopy, input) + sort.Slice(inputCopy, func(i, j int) bool { return inputCopy[i] < inputCopy[j] }) + + midIdx := l / 2 + + if l%2 == 1 { + return inputCopy[midIdx], nil + } + + // The median is an average of the two middle numbers. It's rounded away from zero + // to the nearest integer. + // Note x <= y since `inputCopy` is sorted. + x := inputCopy[midIdx-1] + y := inputCopy[midIdx] + + if x <= 0 && y >= 0 { + // x and y have different signs, so x+y cannot overflow. + sum := x + y + return sum/2 + sum%2, nil + } + + if y > 0 { + // x and y are both positive. + return y - (y-x)/2, nil + } + + // x and y are both negative. + return x + (y-x)/2, nil +} + +func AbsInt32(i int32) uint32 { + if i < 0 { + return uint32(0 - i) + } + + return uint32(i) +} + +func Min[T constraints.Ordered](x, y T) T { + if x > y { + return y + } + return x +} diff --git a/lib/metrics/constants.go b/lib/metrics/constants.go new file mode 100644 index 000000000..3b80bccd6 --- /dev/null +++ b/lib/metrics/constants.go @@ -0,0 +1,153 @@ +package metrics + +// Keep the metric fields alphabetized within each category. +const ( + // Common. + AppVersion = "app_version" + AppInfo = "app_info" + BlockHeight = "block_height" + Count = "count" + Detail = "detail" + Deterministic = "deterministic" + Distribution = "distribution" + Error = "error" + GitCommit = "git_commit" + HttpGet5xx = "http_get_5xx" + HttpGetHangup = "http_get_hangup" + HttpGetRequest = "http_get_request" + HttpGetResponse = "http_get_response" + HttpGetTimeout = "http_get_timeout" + Invalid = "invalid" + Latency = "latency" + Matched = "matched" + MessageType = "message_type" + Msg = "msg" + Negative = "negative" + NonDeterministic = "non_deterministic" + Positive = "positive" + Reason = "reason" + Received = "received" + Rejected = "rejected" + SampleRate = "sample_rate" + SequenceNumber = "sequence_number" + Success = "success" + Valid = "valid" + ValidateBasic = "validate_basic" + CheckTx = "check_tx" + ReCheckTx = "recheck_tx" + DeliverTx = "deliver_tx" + ProcessProposal = "process_proposal" + + // Common (Daemons). + MainTaskLoop = "main_task_loop" + + // ABCI: Prepare / Process + ConsensusRound = "consensus_round" + DisallowMsg = "disallow_msg" + Decode = "decode" + FundingTx = "funding_tx" + GetTxsInOrder = "get_txs_in_order" + Handler = "handler" + NumOtherTxs = "num_other_txs" + OperationsTx = "operations_tx" + OriginalNumTxs = "original_num_txs" + OtherTxs = "other_txs" + RemoveDisallowMsgs = "remove_disallow_msgs" + PrepareProposalTxs = "prepare_proposal_txs" + PrepareCheckState = "prepare_check_state" + PricesTx = "prices_tx" + TotalNumBytes = "total_num_bytes" + TotalNumTxs = "total_num_txs" + Validate = "validate" + + RateLimit = "rate_limit" + + // Daemon + DaemonServer = "daemon_server" + ValidResponse = "valid_response" + MissingResponse = "missing_response" + + // Epochs. + EpochInfoName = "epoch_name" + EpochNumber = "epoch_number" + IsEpochOne = "is_epoch_one" + + // Block Time. + BlockTimeMs = "block_time_ms" + + // Prices. + CreateOracleMarket = "create_oracle_market" + CurrentMarketPrices = "current_market_prices" + GetValidMarketPriceUpdates = "get_valid_market_price_updates" + IndexPriceDoesNotExist = "index_price_does_not_exist" + IndexPriceIsZero = "index_price_is_zero" + IndexPriceNotAccurate = "index_price_not_accurate" + IndexPriceNotAvailForAccuracyCheck = "index_price_not_available_for_accuracy_check" + LastPriceUpdateForMarketBlock = "last_price_update_for_market_block" + MissingPriceUpdates = "missing_price_updates" + NumMarketPricesToUpdate = "num_market_prices_to_update" + PriceChangeRate = "price_change_rate" + ProposedPriceChangesPriceUpdateDecision = "proposed_price_changes_price_update_decision" + ProposedPriceCrossesOraclePrice = "proposed_price_crosses_oracle_price" + ProposedPriceDoesNotMeetMinPriceChange = "proposed_price_does_not_meet_min_price_change" + RecentSmoothedPriceDoesNotMeetMinPriceChange = "recent_smoothed_price_doesnt_meet_min_price_change" + RecentSmoothedPriceCrossesOraclePrice = "recent_smoothed_price_crosses_old_price" + StatefulPriceUpdateValidation = "stateful_price_update_validation" + UpdateMarketParam = "update_market_param" + UpdateMarketPrices = "update_market_prices" + UpdateSmoothedPrices = "update_smoothed_prices" + + // Pricefeed Daemon. + Exchange = "exchange" + ExchangeQueryHandlerApiRequest = "exchange_query_handler_api_request" + ExchangeSpecificError = "exchange_specific_error" + GetAllPrices_MarketIdToPrice = "get_all_prices_market_id_to_price" + PriceEncoderUpdatePrice = "price_encoder_update_price" + PricefeedDaemon = "pricefeed_daemon" + ConfiguredMarketCount = "configured_market_count" + ConfiguredMarketCountPerExchange = "configured_market_count_per_exchange" + ConfiguredExchangeCountPerMarket = "configured_exchange_count_per_market" + MarketUpdaterGetAllMarketParams = "market_updater_get_all_market_params" + MarketUpdaterApplyMarketUpdates = "market_updater_apply_market_updates" + MarketUpdaterUpdateMarkets = "market_updater_update_markets" + PriceEncoderPriceConversion = "price_encoder_price_conversion" + PriceFetcherQueryExchange = "price_fetcher_query_exchange" + PriceFetcherQueryForMarket = "price_fetcher_query_for_market_sampled" + PriceFetcherSubtaskLoop = "price_fetcher_subtask_loop" + PriceFetcherSubtaskLoopAndSetCtxTimeout = "price_fetcher_subtask_loop_and_set_ctx_timeout" + PriceUpdateCount = "price_update_count" + PriceUpdaterSendPrices = "price_updater_send_prices" + PriceUpdaterTaskLoop = "price_updater_task_loop" + PriceUpdaterTransformPrices = "price_updater_transform_prices" + PriceUpdaterZeroPrices = "price_updater_zero_prices" + + // Pricefeed Server. + GetValidPrices = "get_valid_prices" + ValidPrices = "valid_prices" + NoMarketPrice = "no_market_price" + NoValidMedianPrice = "no_valid_median_price" + PricefeedServer = "pricefeed_server" + PricefeedServerUpdatePrices = "pricefeed_server_update_prices" + PricefeedServerValidatePrices = "pricefeed_server_validate_prices" + PriceIsInvalid = "price_is_invalid" + + // Shared Pricefeed Server and Daemon. + UpdatePrice = "update_price" + + // msgsender + MessageSendSuccess = "message_send_success" + MessageSendError = "message_send_error" + SendOffchainData = "send_offchain_data" + SendOnchainData = "send_onchain_data" + OnchainMessageLength = "onchain_message_length" + OffchainMessageLength = "offchain_message_length" + + // Indexer events. + TotalNumIndexerBlockEvents = "total_num_block_events" + TotalNumIndexerTxnEvents = "total_num_txn_events" +) + +const ( + LatencyMetricSampleRate = 0.01 + AvailableMarketsSampleRate = .1 +) diff --git a/lib/metrics/labels.go b/lib/metrics/labels.go new file mode 100644 index 000000000..514bc24e1 --- /dev/null +++ b/lib/metrics/labels.go @@ -0,0 +1,12 @@ +package metrics + +const ( + // Price labels. + ExchangeId = "exchange_id" + MarketId = "market_id" + StatusCode = "status_code" + + Callback = "callback" + + Daemon = "daemon" +) diff --git a/lib/metrics/lib.go b/lib/metrics/lib.go new file mode 100644 index 000000000..bd67d1c41 --- /dev/null +++ b/lib/metrics/lib.go @@ -0,0 +1,92 @@ +package metrics + +import ( + "time" + + gometrics "github.com/armon/go-metrics" + "github.com/cosmos/cosmos-sdk/telemetry" +) + +// This file provides a main entrypoint for logging in the v4 protocol. +// TODO(CLOB-1013) Drop both metrics libraries above for a library +// that supports float64 (i.e hashicorp go-metrics) + +type Label = gometrics.Label + +// IncrCounterWithLabels provides a wrapper functionality for emitting a counter +// metric with global labels (if any) along with the provided labels. +func IncrCounterWithLabels(key string, val float32, labels ...Label) { + telemetry.IncrCounterWithLabels([]string{key}, val, labels) +} + +// IncrCounter provides a wrapper functionality for emitting a counter +// metric with global labels (if any). +func IncrCounter(key string, val float32) { + telemetry.IncrCounterWithLabels([]string{key}, val, []gometrics.Label{}) +} + +// SetGaugeWithLabels provides a wrapper functionality for emitting a gauge +// metric with global labels (if any) along with the provided labels. +func SetGaugeWithLabels(key string, val float32, labels ...gometrics.Label) { + telemetry.SetGaugeWithLabels([]string{key}, val, labels) +} + +// SetGauge provides a wrapper functionality for emitting a gauge +// metric with global labels (if any). +func SetGauge(key string, val float32) { + telemetry.SetGaugeWithLabels([]string{key}, val, []gometrics.Label{}) +} + +// AddSampleWithLabels provides a wrapper functionality for emitting a sample +// metric with the provided labels. +func AddSampleWithLabels(key string, val float32, labels ...gometrics.Label) { + gometrics.AddSampleWithLabels( + []string{key}, + val, + labels, + ) +} + +// AddSample provides a wrapper functionality for emitting a sample +// metric. +func AddSample(key string, val float32) { + gometrics.AddSampleWithLabels( + []string{key}, + val, + []gometrics.Label{}, + ) +} + +// ModuleMeasureSince provides a wrapper functionality for emitting a time measure +// metric with global labels (if any). +// Please try to use `AddSample` instead. +// TODO(CLOB-1022) Roll our own calculations for timing on top of AddSample instead +// of using MeasureSince. +func ModuleMeasureSince(module string, key string, start time.Time) { + telemetry.ModuleMeasureSince( + module, + start, + key, + ) +} + +// ModuleMeasureSinceWithLabels provides a short hand method for emitting a time measure +// metric for a module with labels. Global labels are not included in this metric. +// Please try to use `AddSampleWithLabels` instead. +// TODO(CLOB-1022) Roll our own calculations for timing on top of AddSample instead +// of using MeasureSince. +func ModuleMeasureSinceWithLabels( + module string, + keys []string, + start time.Time, + labels []gometrics.Label, +) { + gometrics.MeasureSinceWithLabels( + keys, + start.UTC(), + append( + []gometrics.Label{telemetry.NewLabel(telemetry.MetricLabelNameModule, module)}, + labels..., + ), + ) +} diff --git a/lib/metrics/metric_keys.go b/lib/metrics/metric_keys.go new file mode 100644 index 000000000..61f0fd25f --- /dev/null +++ b/lib/metrics/metric_keys.go @@ -0,0 +1,22 @@ +// nolint:lll +package metrics + +// Metrics Keys Guidelines +// 1. Be wary of length +// 2. Prefix by module +// 3. Suffix keys with a unit of measurement +// 4. Delimit with '_' +// 5. Information such as callback type should be added as tags, not in key names. +// Example: clob_place_order_count, clob_msg_place_order_latency_ms, clob_operations_queue_length +// clob_expired_stateful_orders_count, clob_processed_orders_ms_total + +// Clob Metrics Keys +const ( + // Measure Since + DaemonGetPreviousBlockInfoLatency = "daemon_get_previous_block_info_latency" + DaemonGetAllMarketPricesLatency = "daemon_get_all_market_prices_latency" + DaemonGetMarketPricesPaginatedLatency = "daemon_get_market_prices_paginated_latency" + DaemonGetAllPerpetualsLatency = "daemon_get_all_perpetuals_latency" + DaemonGetPerpetualsPaginatedLatency = "daemon_get_perpetuals_paginated_latency" + MevLatency = "mev_latency" +) diff --git a/lib/metrics/util.go b/lib/metrics/util.go new file mode 100644 index 000000000..cd16b4261 --- /dev/null +++ b/lib/metrics/util.go @@ -0,0 +1,82 @@ +package metrics + +import ( + "math/big" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + + gometrics "github.com/armon/go-metrics" + "github.com/cosmos/cosmos-sdk/telemetry" +) + +// IncrCountMetricWithLabels increases a count metric from a module with the provided labels by a count of 1. +func IncrCountMetricWithLabels(module string, metric string, labels ...gometrics.Label) { + telemetry.IncrCounterWithLabels( + []string{module, metric, Count}, + 1, + labels, + ) +} + +// IncrSuccessOrErrorCounter increments either the success or error counter for a given handler +// based on whether the given error is nil or not. This function is intended to be called in a +// defer block at the top of any function which returns an error. +func IncrSuccessOrErrorCounter(err error, module string, handler string, callback string, labels ...gometrics.Label) { + successOrError := Success + if err != nil { + successOrError = Error + } + + telemetry.IncrCounterWithLabels( + []string{ + module, + handler, + successOrError, + Count, + }, + 1, + append( + []gometrics.Label{ + GetLabelForStringValue(Callback, callback), + }, + labels..., + ), + ) +} + +// GetLabelForBoolValue returns a telemetry label for a given label and bool value. +func GetLabelForBoolValue(labelName string, labelValue bool) gometrics.Label { + return GetLabelForStringValue(labelName, strconv.FormatBool(labelValue)) +} + +// GetLabelForIntValue returns a telemetry label for a given label and int value. +func GetLabelForIntValue(labelName string, labelValue int) gometrics.Label { + return GetLabelForStringValue(labelName, strconv.Itoa(labelValue)) +} + +// GetLabelForStringValue returns a telemetry label for a given label and string value. +func GetLabelForStringValue(labelName string, labelValue string) gometrics.Label { + return telemetry.NewLabel(labelName, labelValue) +} + +// GetMetricValueFromBigInt returns a telemetry value (float32) from an integer value. +// Any rounding information is ignored, so this function should only be used for metrics. +func GetMetricValueFromBigInt(i *big.Int) float32 { + r, _ := new(big.Float).SetInt(i).Float32() + return r +} + +// GetCallbackMetricFromCtx determines the callback metric based on the context. Note that DeliverTx is implied +// if the context is not CheckTx or ReCheckTx. This function is unable to account for other callbacks like +// PrepareCheckState or EndBlocker. +func GetCallbackMetricFromCtx(ctx sdk.Context) string { + if ctx.IsReCheckTx() { + return ReCheckTx + } + if ctx.IsCheckTx() { + return CheckTx + } + + return DeliverTx +} diff --git a/lib/metrics/util_test.go b/lib/metrics/util_test.go new file mode 100644 index 000000000..ebf2cdf88 --- /dev/null +++ b/lib/metrics/util_test.go @@ -0,0 +1,244 @@ +package metrics_test + +import ( + "math" + "math/big" + "testing" + "time" + + gometrics "github.com/armon/go-metrics" + "github.com/tellor-io/layer/lib/metrics" + big_testutil "github.com/tellor-io/layer/testutil/big" + + "github.com/stretchr/testify/require" +) + +func TestIncrCountMetricWithLabels(t *testing.T) { + t.Cleanup(gometrics.Shutdown) + + conf := gometrics.DefaultConfig("testService") + sink := gometrics.NewInmemSink(time.Hour, time.Hour) + _, err := gometrics.NewGlobal(conf, sink) + require.NoError(t, err) + + for i := 0; i < 3; i++ { + metrics.IncrCountMetricWithLabels( + "testModule", + "testMetric", + gometrics.Label{ + Name: "testLabel", + Value: "testLabelValue", + }, + ) + } + + found := false + for _, metrics := range sink.Data() { + metrics.RLock() + defer metrics.RUnlock() + + if metric, ok := metrics.Counters["testService.testModule.testMetric.count;testLabel=testLabelValue"]; ok { + require.Equal(t, + []gometrics.Label{{ + Name: "testLabel", + Value: "testLabelValue", + }}, + metric.Labels) + require.Equal(t, 3, metric.Count) + require.Equal(t, float64(3), metric.Sum) + found = true + } + } + require.True(t, found) +} + +func TestIncrCountMetricWithLabelsDoesntPanic(t *testing.T) { + require.NotPanics(t, func() { + metrics.IncrCountMetricWithLabels("module", "metric", metrics.GetLabelForBoolValue("label", true)) + }) +} + +func TestGetLabelForBoolValue(t *testing.T) { + tests := map[string]struct { + name string + condition bool + expectedLabelValue string + }{ + "true": { + name: "labelname", + condition: true, + expectedLabelValue: "true", + }, + "false": { + name: "labelname", + condition: false, + expectedLabelValue: "false", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + label := metrics.GetLabelForBoolValue(tc.name, tc.condition) + require.Equal(t, tc.name, label.Name) + require.Equal(t, tc.expectedLabelValue, label.Value) + }) + } +} + +func TestGetLabelForIntValue(t *testing.T) { + tests := map[string]struct { + name string + value int + expectedLabelValue string + }{ + "min": { + name: "labelname", + value: math.MinInt, + expectedLabelValue: "-9223372036854775808", + }, + "negative": { + name: "labelname", + value: -1, + expectedLabelValue: "-1", + }, + "zero": { + name: "labelname", + value: 0, + expectedLabelValue: "0", + }, + "positive": { + name: "labelname", + value: 1, + expectedLabelValue: "1", + }, + "max": { + name: "labelname", + value: math.MaxInt, + expectedLabelValue: "9223372036854775807", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + label := metrics.GetLabelForIntValue(tc.name, tc.value) + require.Equal(t, tc.name, label.Name) + require.Equal(t, tc.expectedLabelValue, label.Value) + }) + } +} + +func TestGetLabelForStringValue(t *testing.T) { + tests := map[string]struct { + name string + value string + expectedLabelValue string + }{ + "empty": { + name: "labelname", + value: "", + expectedLabelValue: "", + }, + "short string": { + name: "labelname", + value: "abc", + expectedLabelValue: "abc", + }, + "long string": { + name: "labelname", + value: "abc def 2389209 lsdf ;'sdf';s#2", + expectedLabelValue: "abc def 2389209 lsdf ;'sdf';s#2", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + label := metrics.GetLabelForStringValue(tc.name, tc.value) + require.Equal(t, tc.name, label.Name) + require.Equal(t, tc.expectedLabelValue, label.Value) + }) + } +} + +func TestGetMetricValueFromBigInt(t *testing.T) { + tests := map[string]struct { + input *big.Int + expected float32 + }{ + "zero": { + input: big.NewInt(0), + expected: float32(0), + }, + "positive": { + input: big.NewInt(1234), + expected: float32(1234), + }, + "negative": { + input: big.NewInt(-1234), + expected: float32(-1234), + }, + "underflow": { + input: big.NewInt(math.MinInt), + expected: float32(-9.223372e+18), + }, + "overflow": { + input: new(big.Int).SetUint64(math.MaxUint64), + expected: float32(1.8446744e+19), + }, + "overflow: 1234567 * 1e24": { + input: big_testutil.Int64MulPow10(1234567, 24), // 1234567 * 1e24 + expected: float32(1.234567e+30), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require.Equal(t, tc.expected, metrics.GetMetricValueFromBigInt(tc.input)) + }) + } +} + +func TestModuleMeasureSinceWithLabels(t *testing.T) { + t.Cleanup(gometrics.Shutdown) + + conf := gometrics.DefaultConfig("testService") + sink := gometrics.NewInmemSink(time.Hour, time.Hour) + _, err := gometrics.NewGlobal(conf, sink) + require.NoError(t, err) + + for i := 0; i < 3; i++ { + metrics.ModuleMeasureSinceWithLabels( + "testModule", + []string{"testKey1", "testKey2"}, + time.Now(), + []gometrics.Label{{ + Name: "testLabel", + Value: "testLabelValue", + }}, + ) + } + + found := false + for _, metrics := range sink.Data() { + metrics.RLock() + defer metrics.RUnlock() + + if metric, ok := metrics.Samples["testService.testKey1.testKey2;module=testModule;testLabel=testLabelValue"]; ok { + require.Equal(t, + []gometrics.Label{ + { + Name: "module", + Value: "testModule", + }, + { + Name: "testLabel", + Value: "testLabelValue", + }, + }, + metric.Labels) + require.Equal(t, 3, metric.Count) + // Since we can't inject time into gometrics we can't calculate the exact expected timing sample + // so we bound the value between 0 and 3 seconds assuming that hardware isn't so overloaded that + // executing this test takes longer than 3 seconds. + require.Less(t, 0.0, metric.Sum) + require.Greater(t, 3.0, metric.Sum) + found = true + } + } + require.True(t, found) +} diff --git a/lib/prices/utils.go b/lib/prices/utils.go new file mode 100644 index 000000000..2ffcd0767 --- /dev/null +++ b/lib/prices/utils.go @@ -0,0 +1,101 @@ +package prices + +import ( + "math/big" + + "github.com/shopspring/decimal" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +/* + * Price conversion functions + * + * At this time, the protocol represents prices as a tuple of (price, exponent), where the raw price the asset can be + * inferred as + * + * rawPrice = price * 10 ^ exponent + * + * Price exponents are chosen on a per-market basis, so price conversions that involve multiple market prices require + * converting to and from raw prices in order to perform the conversion math. + * + * For price conversion math, we use the decimal package found here: + * https://pkg.go.dev/github.com/shopspring/decimal#section-readme + * + * The decimal package has some benefits over big.Rat that can make it more suitable for representing money - see + * the readme. However, it is not as performant as big.Rat, so we only use it for price conversion math. + */ + +// Invert inverts a price, returning the inverted price multiplied by 10^-exponent. +// This method is meant to be used for inverting stablecoin ticker prices. For the sake of precision, price inversion +// is only intended to be used for pricing markets that are close to 1:1 price-wise, e.g. USD-USDT. Inverting a price +// that is <<>> 1 could result in a loss of precision. +func Invert(price uint64, exponent types.Exponent) uint64 { + // Invert 0 to 0. Zero prices are invalid and are prevented by the price fetcher, but we insert this guard just + // in case to prevent any panics. + if price == 0 { + return 0 + } + + decimalPrice := decimal.NewFromBigInt(new(big.Int).SetUint64(price), exponent) + invertedPrice := decimal.NewFromInt(1).Div(decimalPrice).Mul( + decimal.NewFromBigInt(new(big.Int).SetUint64(1), -exponent), + ).BigInt().Uint64() + return invertedPrice +} + +// Multiply multiplies two prices, returning the resulting price as a uint64 multiplied by the first exponent. +// +// Formula: rawPrice = price * 10 ^ exponent +// +// rawAdjustByPrice = adjustByPrice * 10 ^ adjustByExponent +// rawAdjustedPrice = rawPrice * rawAdjustByPrice +// adjustedPrice = rawAdjustedPrice * 10 ^ -exponent +// +// The most common use case of multiply will be to convert a market price from one stablecoin to another. +// For example, 1INCH-USD = 1INCH_USDT * USDT-USD. +func Multiply(price uint64, exponent int32, adjustByPrice uint64, adjustByExponent int32) (adjustedPrice uint64) { + decimalPrice := decimal.NewFromBigInt(new(big.Int).SetUint64(price), exponent) + decimalAdjustByPrice := decimal.NewFromBigInt(new(big.Int).SetUint64(adjustByPrice), adjustByExponent) + adjustedPrice = decimalPrice.Mul(decimalAdjustByPrice).Mul( + decimal.NewFromBigInt(new(big.Int).SetUint64(1), -exponent), + ).BigInt().Uint64() + return adjustedPrice +} + +// Divide divides two prices, returning the resulting price as a uint64 multiplied by the divisor price's exponent. +// +// Formula: rawPrice = price * 10 ^ exponent +// +// rawAdjustByPrice = adjustByPrice * 10 ^ adjustByExponent +// rawAdjustedPrice = rawAdjustByPrice / rawPrice +// adjustedPrice = rawAdjustedPrice * 10 ^ -exponent +// +// This price conversion method is typically used in practice to derive stablecoin prices by dividing crypto asset +// prices in two different stablecoin quote currencies: for example, USDT-USD = BTC-USD / BTC-USDT. +func Divide( + adjustByPrice uint64, + adjustByExponent types.Exponent, + price uint64, + exponent types.Exponent, +) (adjustedPrice uint64) { + // Zero prices are invalid and are prevented by the price fetcher, but we insert this guard just in case to prevent + // any panics. + if price == 0 { + return 0 + } + + decimalPrice := decimal.NewFromBigInt(new(big.Int).SetUint64(price), exponent) + decimalAdjustByPrice := decimal.NewFromBigInt(new(big.Int).SetUint64(adjustByPrice), adjustByExponent) + adjustedPrice = decimalAdjustByPrice.Div(decimalPrice).Mul( + decimal.NewFromBigInt(new(big.Int).SetUint64(1), -exponent), + ).BigInt().Uint64() + return adjustedPrice +} + +// PriceToFloat32ForLogging converts a price, exponent to a float32 for logging purposes. This is not meant to be used +// for price calucations within the protocol, as it could result in an arbitrary loss of precision. +func PriceToFloat32ForLogging(price uint64, exponent types.Exponent) float32 { + // We're not concerned about truncation here. + priceFloat32, _ := decimal.NewFromBigInt(new(big.Int).SetUint64(price), exponent).BigFloat().Float32() + return priceFloat32 +} diff --git a/lib/sortable.go b/lib/sortable.go new file mode 100644 index 000000000..239c91d99 --- /dev/null +++ b/lib/sortable.go @@ -0,0 +1,26 @@ +package lib + +import ( + "sort" + + "golang.org/x/exp/constraints" +) + +// Sortable[K] attaches the methods of sort.Interface to []K, sorting in increasing order. +type Sortable[K constraints.Ordered] []K + +// The below methods are required to implement `sort.Interface` for sorting using the sort package. +// int is used as an example. +var _ sort.Interface = Sortable[int]{} + +func (s Sortable[K]) Len() int { + return len(s) +} + +func (s Sortable[K]) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Sortable[K]) Less(i, j int) bool { + return s[i] < s[j] +} diff --git a/lib/time/time_provider.go b/lib/time/time_provider.go new file mode 100644 index 000000000..955c27895 --- /dev/null +++ b/lib/time/time_provider.go @@ -0,0 +1,21 @@ +package time + +import ( + "time" +) + +// Ensure the `TimeProviderImpl` is implemented at compile time. +var _ TimeProvider = &TimeProviderImpl{} + +// TimeProvider is an interface that provides time. +type TimeProvider interface { + Now() time.Time +} + +// TimeProviderImpl implements TimeProvider interface. +type TimeProviderImpl struct{} + +// Now returns current time. +func (t *TimeProviderImpl) Now() time.Time { + return time.Now() +} diff --git a/mocks/AppOptions.go b/mocks/AppOptions.go new file mode 100644 index 000000000..a53be4095 --- /dev/null +++ b/mocks/AppOptions.go @@ -0,0 +1,41 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// AppOptions is an autogenerated mock type for the AppOptions type +type AppOptions struct { + mock.Mock +} + +// Get provides a mock function with given fields: _a0 +func (_m *AppOptions) Get(_a0 string) interface{} { + ret := _m.Called(_a0) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(string) interface{}); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +type mockConstructorTestingTNewAppOptions interface { + mock.TestingT + Cleanup(func()) +} + +// NewAppOptions creates a new instance of AppOptions. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAppOptions(t mockConstructorTestingTNewAppOptions) *AppOptions { + mock := &AppOptions{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/proto/layer/daemons/pricefeed.proto b/proto/layer/daemons/pricefeed.proto new file mode 100644 index 000000000..6041e4a24 --- /dev/null +++ b/proto/layer/daemons/pricefeed.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; +package layer.daemons; + +import "gogoproto/gogo.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/tellor-io/layer/daemons/server/types"; + +// PriceFeedService provides methods related to market prices. +service PriceFeedService { + // Updates market prices. + rpc UpdateMarketPrices(UpdateMarketPricesRequest) returns (UpdateMarketPricesResponse) {} +} + +// UpdateMarketPriceRequest is a request message updating market prices. +message UpdateMarketPricesRequest { + repeated MarketPriceUpdate market_price_updates = 1; +} + +// UpdateMarketPricesResponse is a response message for updating market prices. +message UpdateMarketPricesResponse { + repeated MarketPriceUpdate market_price_updates = 1; +} + +// ExchangePrice represents a specific exchange's market price +message ExchangePrice { + string exchange_id = 1; + uint64 price = 2; + google.protobuf.Timestamp last_update_time = 3 + [ (gogoproto.nullable) = true, (gogoproto.stdtime) = true ]; +} + +// MarketPriceUpdate represents an update to a single market +message MarketPriceUpdate { + uint32 market_id = 1; + repeated ExchangePrice exchange_prices = 2; +} diff --git a/proto/layer/oracle/median_values.proto b/proto/layer/oracle/median_values.proto new file mode 100644 index 000000000..a45cdcec0 --- /dev/null +++ b/proto/layer/oracle/median_values.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; +package layer.oracle; + +option go_package = "github.com/tellor-io/layer/x/oracle/types"; + +message MedianValues { + + uint32 marketId = 1; + uint64 price = 2; +} diff --git a/proto/layer/oracle/query.proto b/proto/layer/oracle/query.proto index 8beec9d5e..7bd7d5737 100644 --- a/proto/layer/oracle/query.proto +++ b/proto/layer/oracle/query.proto @@ -13,6 +13,7 @@ import "layer/oracle/user_tip.proto"; import "layer/oracle/aggregate.proto"; import "amino/amino.proto"; import "cosmos/base/v1beta1/coin.proto"; +import "layer/oracle/median_values.proto"; option go_package = "github.com/tellor-io/layer/x/oracle/types"; @@ -68,6 +69,12 @@ service Query { option (google.api.http).get = "/tellor-io/layer/oracle/get_time_based_rewards"; } + + // Queries a list of GetMedianValues items. + rpc GetMedianValues (QueryGetMedianValuesRequest) returns (QueryGetMedianValuesResponse) { + option (google.api.http).get = "/tellor-io/layer/oracle/get_median_values"; + + } } // QueryParamsRequest is request type for the Query/Params RPC method. message QueryParamsRequest {} @@ -133,10 +140,12 @@ message QueryGetDataBeforeRequest { message QueryGetTimeBasedRewardsRequest {} message QueryGetTimeBasedRewardsResponse { - cosmos.base.v1beta1.Coin reward = 1 [ - (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coin", - (gogoproto.nullable) = false, - (amino.dont_omitempty) = true - ]; + cosmos.base.v1beta1.Coin reward = 1 [(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coin", (gogoproto.nullable) = false, (amino.dont_omitempty) = true]; +} + +message QueryGetMedianValuesRequest {} + +message QueryGetMedianValuesResponse { + repeated MedianValues medianValues = 1; } diff --git a/testutil/big/big.go b/testutil/big/big.go new file mode 100644 index 000000000..f7863cada --- /dev/null +++ b/testutil/big/big.go @@ -0,0 +1,30 @@ +// Package big provides testing utility methods for the "math/big" library. +package big + +import ( + "math/big" + + "github.com/tellor-io/layer/lib" +) + +// MustFirst is used for returning the first value of the SetString +// method on a `*big.Int` or `*big.Rat`. This will panic if the conversion fails. +func MustFirst[T *big.Int | *big.Rat](n T, success bool) T { + if !success { + panic("Conversion failed") + } + return n +} + +// Int64MulPow10 returns the result of `val * 10^exponent`, in *big.Int. +func Int64MulPow10( + val int64, + exponent uint64, +) ( + result *big.Int, +) { + return new(big.Int).Mul( + big.NewInt(val), + lib.BigPow10(exponent), + ) +} diff --git a/testutil/constants/prices.go b/testutil/constants/prices.go new file mode 100644 index 000000000..a2e4dad59 --- /dev/null +++ b/testutil/constants/prices.go @@ -0,0 +1,6 @@ +package constants + +const ( + BtcUsdExponent = -5 + EthUsdExponent = -6 +) diff --git a/testutil/daemons/pricefeed/exchange_config/market_id.go b/testutil/daemons/pricefeed/exchange_config/market_id.go new file mode 100644 index 000000000..146d90272 --- /dev/null +++ b/testutil/daemons/pricefeed/exchange_config/market_id.go @@ -0,0 +1,83 @@ +package exchange_config + +import "github.com/tellor-io/layer/daemons/pricefeed/client/types" + +// All market ids must match with the genesis state. +const ( + // MARKET_BTC_USD is the id for the BTC-USD market pair. + MARKET_BTC_USD types.MarketId = 0 + // MARKET_ETH_USD is the id for the ETH-USD market pair. + MARKET_ETH_USD types.MarketId = 1 + // MARKET_LINK_USD is the id for the LINK-USD market pair. + MARKET_LINK_USD types.MarketId = 2 + // MARKET_MATIC_USD is the id for the MATIC-USD market pair. + MARKET_MATIC_USD types.MarketId = 3 + // MARKET_CRV_USD is the id for the CRV-USD market pair. + MARKET_CRV_USD types.MarketId = 4 + // MARKET_SOL_USD is the id for the SOL-USD market pair. + MARKET_SOL_USD types.MarketId = 5 + // MARKET_ADA_USD is the id for the ADA-USD market pair. + MARKET_ADA_USD types.MarketId = 6 + // MARKET_AVAX_USD is the id for the AVAX-USD market pair. + MARKET_AVAX_USD types.MarketId = 7 + // MARKET_FIL_USD is the id for the FIL-USD market pair. + MARKET_FIL_USD types.MarketId = 8 + // MARKET_LTC_USD is the id for the LTC-USD market pair. + MARKET_LTC_USD types.MarketId = 9 + // MARKET_DOGE_USD is the id for the DOGE-USD market pair. + MARKET_DOGE_USD types.MarketId = 10 + // MARKET_ATOM_USD is the id for the ATOM-USD market pair. + MARKET_ATOM_USD types.MarketId = 11 + // MARKET_DOT_USD is the id for the DOT-USD market pair. + MARKET_DOT_USD types.MarketId = 12 + // MARKET_UNI_USD is the id for the UNI-USD market pair. + MARKET_UNI_USD types.MarketId = 13 + // MARKET_BCH_USD is the id for the BCH-USD market pair. + MARKET_BCH_USD types.MarketId = 14 + // MARKET_TRX_USD is the id for the TRX-USD market pair. + MARKET_TRX_USD types.MarketId = 15 + // MARKET_NEAR_USD is the id for the NEAR-USD market pair. + MARKET_NEAR_USD types.MarketId = 16 + // MARKET_MKR_USD is the id for the MKR-USD market pair. + MARKET_MKR_USD types.MarketId = 17 + // MARKET_XLM_USD is the id for the XLM-USD market pair. + MARKET_XLM_USD types.MarketId = 18 + // MARKET_ETC_USD is the id for the ETC-USD market pair. + MARKET_ETC_USD types.MarketId = 19 + // MARKET_COMP_USD is the id for the COMP-USD market pair. + MARKET_COMP_USD types.MarketId = 20 + // MARKET_WLD_USD is the id for the WLD-USD market pair. + MARKET_WLD_USD types.MarketId = 21 + // MARKET_APE_USD is the id for the APE-USD market pair. + MARKET_APE_USD types.MarketId = 22 + // MARKET_APT_USD is the id for the APT-USD market pair. + MARKET_APT_USD types.MarketId = 23 + // MARKET_ARB_USD is the id for the ARB-USD market pair. + MARKET_ARB_USD types.MarketId = 24 + // MARKET_BLUR_USD is the id for the BLUR-USD market pair. + MARKET_BLUR_USD types.MarketId = 25 + // MARKET_LDO_USD is the id for the LDO-USD market pair. + MARKET_LDO_USD types.MarketId = 26 + // MARKET_OP_USD is the id for the OP-USD market pair. + MARKET_OP_USD types.MarketId = 27 + // MARKET_PEPE_USD is the id for the PEPE-USD market pair. + MARKET_PEPE_USD types.MarketId = 28 + // MARKET_SEI_USD is the id for the SEI-USD market pair. + MARKET_SEI_USD types.MarketId = 29 + // MARKET_SHIB_USD is the id for the SHIB-USD market pair. + MARKET_SHIB_USD types.MarketId = 30 + // MARKET_SUI_USD is the id for the SUI-USD market pair. + MARKET_SUI_USD types.MarketId = 31 + // MARKET_XRP_USD is the id for the XRP-USD market pair. + MARKET_XRP_USD types.MarketId = 32 + + // Testing markets used in local, staging, dev + // MARKET_TEST_USD is the id used for the TEST-USD market pair. + MARKET_TEST_USD types.MarketId = 33 + + // Non-trading markets. + // MARKET_USDT_USD is the id for the USDT-USD market pair. + MARKET_USDT_USD types.MarketId = 1_000_000 + // MARKET_DYDX_USD is the id for the DYDX-USD market pair. + MARKET_DYDX_USD types.MarketId = 1_000_001 +) diff --git a/testutil/daemons/pricefeed/exchange_config/testnet_exchange_market_config.go b/testutil/daemons/pricefeed/exchange_config/testnet_exchange_market_config.go new file mode 100644 index 000000000..3a48c8d0a --- /dev/null +++ b/testutil/daemons/pricefeed/exchange_config/testnet_exchange_market_config.go @@ -0,0 +1,1103 @@ +package exchange_config + +import ( + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + // TestnetExchangeMarketConfig maps exchange feed ids to exchange market config. This map is used to generate + // the exchange config json used to construct the genesis file for various testnet deploys defined in the testing + // package - namely, localnet, dev, and staging. Note that public testnet is not affected by this map. + TestnetExchangeMarketConfig = map[types.ExchangeId]*types.MutableExchangeMarketConfig{ + exchange_common.EXCHANGE_ID_BINANCE: { + Id: exchange_common.EXCHANGE_ID_BINANCE, + // example `symbols` parameter: ["BTCUSDT","BNBUSDT"] + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_DYDX_USD: { + Ticker: "DYDXUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BTC_USD: { + Ticker: "BTCUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETH_USD: { + Ticker: "ETHUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LINK_USD: { + Ticker: "LINKUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MATIC_USD: { + Ticker: "MATICUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_CRV_USD: { + Ticker: "CRVUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SOL_USD: { + Ticker: "SOLUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "ADAUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_AVAX_USD: { + Ticker: "AVAXUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_FIL_USD: { + Ticker: "FILUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LTC_USD: { + Ticker: "LTCUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "DOGEUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ATOM_USD: { + Ticker: "ATOMUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOT_USD: { + Ticker: "DOTUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_UNI_USD: { + Ticker: "UNIUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "BCHUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "TRXUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_NEAR_USD: { + Ticker: "NEARUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MKR_USD: { + Ticker: "MKRUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XLM_USD: { + Ticker: "XLMUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETC_USD: { + Ticker: "ETCUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_COMP_USD: { + Ticker: "COMPUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APE_USD: { + Ticker: "APEUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "APTUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "ARBUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LDO_USD: { + Ticker: "LDOUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_OP_USD: { + Ticker: "OPUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_PEPE_USD: { + Ticker: "PEPEUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SEI_USD: { + Ticker: "SEIUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SHIB_USD: { + Ticker: "SHIBUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "SUIUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "WLDUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "XRPUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_USDT_USD: { + Ticker: "USDCUSDT", + Invert: true, + }, + }, + }, + exchange_common.EXCHANGE_ID_KRAKEN: { + Id: exchange_common.EXCHANGE_ID_KRAKEN, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_BTC_USD: { + Ticker: "XXBTZUSD", + }, + MARKET_ETH_USD: { + Ticker: "XETHZUSD", + }, + MARKET_LINK_USD: { + Ticker: "LINKUSD", + }, + MARKET_SOL_USD: { + Ticker: "SOLUSD", + }, + MARKET_ADA_USD: { + Ticker: "ADAUSD", + }, + MARKET_FIL_USD: { + Ticker: "FILUSD", + }, + MARKET_LTC_USD: { + Ticker: "XLTCZUSD", + }, + MARKET_ATOM_USD: { + Ticker: "ATOMUSD", + }, + MARKET_DOT_USD: { + Ticker: "DOTUSD", + }, + MARKET_BCH_USD: { + Ticker: "BCHUSD", + }, + MARKET_XLM_USD: { + Ticker: "XXLMZUSD", + }, + MARKET_APE_USD: { + Ticker: "APEUSD", + }, + MARKET_BLUR_USD: { + Ticker: "BLURUSD", + }, + MARKET_PEPE_USD: { + Ticker: "PEPEUSD", + }, + MARKET_SHIB_USD: { + Ticker: "SHIBUSD", + }, + MARKET_XRP_USD: { + Ticker: "XXRPZUSD", + }, + MARKET_UNI_USD: { + Ticker: "UNIUSD", + }, + MARKET_CRV_USD: { + Ticker: "CRVUSD", + }, + MARKET_COMP_USD: { + Ticker: "COMPUSD", + }, + MARKET_AVAX_USD: { + Ticker: "AVAXUSD", + }, + MARKET_DOGE_USD: { + Ticker: "XDGUSD", + }, + MARKET_LDO_USD: { + Ticker: "LDOUSD", + }, + MARKET_USDT_USD: { + Ticker: "USDTZUSD", + }, + MARKET_MATIC_USD: { + Ticker: "MATICUSD", + }, + MARKET_MKR_USD: { + Ticker: "MKRUSD", + }, + MARKET_TRX_USD: { + Ticker: "TRXUSD", + }, + }, + }, + exchange_common.EXCHANGE_ID_GATE: { + Id: exchange_common.EXCHANGE_ID_GATE, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_DYDX_USD: { + Ticker: "DYDX_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MATIC_USD: { + Ticker: "MATIC_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_CRV_USD: { + Ticker: "CRV_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "ADA_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "DOGE_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOT_USD: { + Ticker: "DOT_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_UNI_USD: { + Ticker: "UNI_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "BCH_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "TRX_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_NEAR_USD: { + Ticker: "NEAR_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETC_USD: { + Ticker: "ETC_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APE_USD: { + Ticker: "APE_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "APT_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "ARB_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BLUR_USD: { + Ticker: "BLUR_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_FIL_USD: { + Ticker: "FIL_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_OP_USD: { + Ticker: "OP_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_PEPE_USD: { + Ticker: "PEPE_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SEI_USD: { + Ticker: "SEI_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SHIB_USD: { + Ticker: "SHIB_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "SUI_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "WLD_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "XRP_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_AVAX_USD: { + Ticker: "AVAX_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ATOM_USD: { + Ticker: "ATOM_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_COMP_USD: { + Ticker: "COMP_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + }, + }, + exchange_common.EXCHANGE_ID_BITSTAMP: { + Id: exchange_common.EXCHANGE_ID_BITSTAMP, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{}, + }, + exchange_common.EXCHANGE_ID_BYBIT: { + Id: exchange_common.EXCHANGE_ID_BYBIT, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_DYDX_USD: { + Ticker: "DYDXUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BTC_USD: { + Ticker: "BTCUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETH_USD: { + Ticker: "ETHUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "XRPUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LTC_USD: { + Ticker: "LTCUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ATOM_USD: { + Ticker: "ATOMUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_UNI_USD: { + Ticker: "UNIUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "WLDUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "APTUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SOL_USD: { + Ticker: "SOLUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "DOGEUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "ADAUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XLM_USD: { + Ticker: "XLMUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SHIB_USD: { + Ticker: "SHIBUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LINK_USD: { + Ticker: "LINKUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "ARBUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "SUIUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "TRXUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SEI_USD: { + Ticker: "SEIUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_PEPE_USD: { + Ticker: "PEPEUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_AVAX_USD: { + Ticker: "AVAXUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "BCHUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOT_USD: { + Ticker: "DOTUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MATIC_USD: { + Ticker: "MATICUSDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_USDT_USD: { + Ticker: "USDCUSDT", + Invert: true, + }, + }, + }, + exchange_common.EXCHANGE_ID_CRYPTO_COM: { + Id: exchange_common.EXCHANGE_ID_CRYPTO_COM, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{}, + }, + exchange_common.EXCHANGE_ID_HUOBI: { + Id: exchange_common.EXCHANGE_ID_HUOBI, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_MATIC_USD: { + Ticker: "maticusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SOL_USD: { + Ticker: "solusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "adausdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_FIL_USD: { + Ticker: "filusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LTC_USD: { + Ticker: "ltcusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "dogeusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "bchusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "trxusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "aptusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "arbusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SEI_USD: { + Ticker: "seiusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "suiusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "wldusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "xrpusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_AVAX_USD: { + Ticker: "avaxusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_NEAR_USD: { + Ticker: "nearusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETC_USD: { + Ticker: "etcusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BTC_USD: { + Ticker: "btcusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETH_USD: { + Ticker: "ethusdt", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_USDT_USD: { + Ticker: "ethusdt", // Adjusted with ETH index price. + AdjustByMarket: newMarketIdWithValue(MARKET_ETH_USD), + Invert: true, + }, + }, + }, + exchange_common.EXCHANGE_ID_KUCOIN: { + Id: exchange_common.EXCHANGE_ID_KUCOIN, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_DYDX_USD: { + Ticker: "DYDX-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LINK_USD: { + Ticker: "LINK-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MATIC_USD: { + Ticker: "MATIC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_CRV_USD: { + Ticker: "CRV-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SOL_USD: { + Ticker: "SOL-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "ADA-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_AVAX_USD: { + Ticker: "AVAX-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LTC_USD: { + Ticker: "LTC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "DOGE-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ATOM_USD: { + Ticker: "ATOM-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOT_USD: { + Ticker: "DOT-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XLM_USD: { + Ticker: "XLM-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "BCH-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "TRX-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "ARB-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BLUR_USD: { + Ticker: "BLUR-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LDO_USD: { + Ticker: "LDO-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_OP_USD: { + Ticker: "OP-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_PEPE_USD: { + Ticker: "PEPE-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SEI_USD: { + Ticker: "SEI-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SHIB_USD: { + Ticker: "SHIB-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "SUI-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "WLD-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "XRP-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MKR_USD: { + Ticker: "MKR-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_NEAR_USD: { + Ticker: "NEAR-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APE_USD: { + Ticker: "APE-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "APT-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_USDT_USD: { + Ticker: "BTC-USDT", // Adjusted with BTC index price. + AdjustByMarket: newMarketIdWithValue(MARKET_BTC_USD), + Invert: true, + }, + MARKET_BTC_USD: { + Ticker: "BTC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETC_USD: { + Ticker: "ETC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETH_USD: { + Ticker: "ETH-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_UNI_USD: { + Ticker: "UNI-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + }, + }, + exchange_common.EXCHANGE_ID_OKX: { + Id: exchange_common.EXCHANGE_ID_OKX, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_DYDX_USD: { + Ticker: "DYDX-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BTC_USD: { + Ticker: "BTC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETH_USD: { + Ticker: "ETH-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LINK_USD: { + Ticker: "LINK-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MATIC_USD: { + Ticker: "MATIC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_CRV_USD: { + Ticker: "CRV-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SOL_USD: { + Ticker: "SOL-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_AVAX_USD: { + Ticker: "AVAX-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_FIL_USD: { + Ticker: "FIL-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LTC_USD: { + Ticker: "LTC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "DOGE-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOT_USD: { + Ticker: "DOT-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_UNI_USD: { + Ticker: "UNI-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "BCH-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "TRX-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETC_USD: { + Ticker: "ETC-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APE_USD: { + Ticker: "APE-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "ARB-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BLUR_USD: { + Ticker: "BLUR-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_OP_USD: { + Ticker: "OP-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_PEPE_USD: { + Ticker: "PEPE-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SHIB_USD: { + Ticker: "SHIB-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "SUI-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "WLD-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "XRP-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_COMP_USD: { + Ticker: "COMP-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MKR_USD: { + Ticker: "MKR-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "APT-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ATOM_USD: { + Ticker: "ATOM-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "ADA-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LDO_USD: { + Ticker: "LDO-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_USDT_USD: { + Ticker: "USDC-USDT", + Invert: true, + }, + MARKET_XLM_USD: { + Ticker: "XLM-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_NEAR_USD: { + Ticker: "NEAR-USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + }, + }, + exchange_common.EXCHANGE_ID_MEXC: { + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_DYDX_USD: { + Ticker: "DYDX_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BTC_USD: { + Ticker: "BTC_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SOL_USD: { + Ticker: "SOL_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LTC_USD: { + Ticker: "LTC_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APE_USD: { + Ticker: "APE_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_APT_USD: { + Ticker: "APT_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ARB_USD: { + Ticker: "ARB_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BLUR_USD: { + Ticker: "BLUR_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_FIL_USD: { + Ticker: "FIL_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LDO_USD: { + Ticker: "LDO_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_OP_USD: { + Ticker: "OP_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_PEPE_USD: { + Ticker: "PEPE_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SEI_USD: { + Ticker: "SEI_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SHIB_USD: { + Ticker: "SHIB_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_SUI_USD: { + Ticker: "SUI_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_WLD_USD: { + Ticker: "WLD_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XLM_USD: { + Ticker: "XLM_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_XRP_USD: { + Ticker: "XRP_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETH_USD: { + Ticker: "ETH_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ADA_USD: { + Ticker: "ADA_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_LINK_USD: { + Ticker: "LINK_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_TRX_USD: { + Ticker: "TRX_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOGE_USD: { + Ticker: "DOGE_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MKR_USD: { + Ticker: "MKR_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_NEAR_USD: { + Ticker: "NEAR_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ATOM_USD: { + Ticker: "ATOM_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_BCH_USD: { + Ticker: "BCH_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_COMP_USD: { + Ticker: "COMP_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_CRV_USD: { + Ticker: "CRV_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_DOT_USD: { + Ticker: "DOT_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_ETC_USD: { + Ticker: "ETC_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + MARKET_MATIC_USD: { + Ticker: "MATIC_USDT", + AdjustByMarket: newMarketIdWithValue(MARKET_USDT_USD), + }, + }, + }, + exchange_common.EXCHANGE_ID_COINBASE_PRO: { + Id: exchange_common.EXCHANGE_ID_COINBASE_PRO, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_BTC_USD: { + Ticker: "BTC-USD", + }, + MARKET_ETH_USD: { + Ticker: "ETH-USD", + }, + MARKET_LINK_USD: { + Ticker: "LINK-USD", + }, + MARKET_MATIC_USD: { + Ticker: "MATIC-USD", + }, + MARKET_CRV_USD: { + Ticker: "CRV-USD", + }, + MARKET_SOL_USD: { + Ticker: "SOL-USD", + }, + MARKET_ADA_USD: { + Ticker: "ADA-USD", + }, + MARKET_FIL_USD: { + Ticker: "FIL-USD", + }, + MARKET_LTC_USD: { + Ticker: "LTC-USD", + }, + MARKET_ATOM_USD: { + Ticker: "ATOM-USD", + }, + MARKET_UNI_USD: { + Ticker: "UNI-USD", + }, + MARKET_BCH_USD: { + Ticker: "BCH-USD", + }, + MARKET_NEAR_USD: { + Ticker: "NEAR-USD", + }, + MARKET_MKR_USD: { + Ticker: "MKR-USD", + }, + MARKET_XLM_USD: { + Ticker: "XLM-USD", + }, + MARKET_ETC_USD: { + Ticker: "ETC-USD", + }, + MARKET_COMP_USD: { + Ticker: "COMP-USD", + }, + MARKET_APE_USD: { + Ticker: "APE-USD", + }, + MARKET_APT_USD: { + Ticker: "APT-USD", + }, + MARKET_ARB_USD: { + Ticker: "ARB-USD", + }, + MARKET_BLUR_USD: { + Ticker: "BLUR-USD", + }, + MARKET_LDO_USD: { + Ticker: "LDO-USD", + }, + MARKET_OP_USD: { + Ticker: "OP-USD", + }, + MARKET_SEI_USD: { + Ticker: "SEI-USD", + }, + MARKET_SHIB_USD: { + Ticker: "SHIB-USD", + }, + MARKET_SUI_USD: { + Ticker: "SUI-USD", + }, + MARKET_XRP_USD: { + Ticker: "XRP-USD", + }, + MARKET_AVAX_USD: { + Ticker: "AVAX-USD", + }, + MARKET_DOGE_USD: { + Ticker: "DOGE-USD", + }, + MARKET_DOT_USD: { + Ticker: "DOT-USD", + }, + MARKET_USDT_USD: { + Ticker: "USDT-USD", + }, + }, + }, + exchange_common.EXCHANGE_ID_TEST_EXCHANGE: { + Id: exchange_common.EXCHANGE_ID_TEST_EXCHANGE, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_BTC_USD: { + Ticker: "BTC-USD", + }, + MARKET_ETH_USD: { + Ticker: "ETH-USD", + }, + MARKET_LINK_USD: { + Ticker: "LINK-USD", + }, + MARKET_USDT_USD: { + Ticker: "USDT-USD", + }, + }, + }, + exchange_common.EXCHANGE_ID_TEST_VOLATILE_EXCHANGE: { + Id: exchange_common.EXCHANGE_ID_TEST_VOLATILE_EXCHANGE, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_TEST_USD: { + Ticker: "TEST-USD", + }, + }, + }, + exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE: { + Id: exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + MARKET_BTC_USD: { + Ticker: "BTC-USD", + }, + MARKET_ETH_USD: { + Ticker: "ETH-USD", + }, + MARKET_SOL_USD: { + Ticker: "SOL-USD", + }, + }, + }, + } +) + +// newMarketIdWithValue returns a pointer to a new market id set to the specified value. This helper method +// is used to initialize the `AdjustByMarket` field of the `MarketConfig` structs above. +func newMarketIdWithValue(id types.MarketId) *types.MarketId { + ptr := new(types.MarketId) + *ptr = id + return ptr +} diff --git a/testutil/daemons/pricefeed/pricefeed_utils.go b/testutil/daemons/pricefeed/pricefeed_utils.go new file mode 100644 index 000000000..cce44cf87 --- /dev/null +++ b/testutil/daemons/pricefeed/pricefeed_utils.go @@ -0,0 +1,38 @@ +package pricefeed + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// ReadJsonTestFile takes a test file with human-readable, formatted JSON, load it, and compacts it. +// The purpose is to remove the formatting (e.g. newlines, tabs, etc) and return a string that would match an +// unmarshaled object string generated by a Go program natively. +func ReadJsonTestFile(t *testing.T, fileName string) string { + fileBytes, err := os.ReadFile(fmt.Sprintf("testdata/%v", fileName)) + require.NoError(t, err, "Error reading test file") + return CompactJsonString(t, string(fileBytes)) +} + +func CompactJsonString(t *testing.T, jsonString string) string { + buffer := &bytes.Buffer{} + err := json.Compact(buffer, []byte(jsonString)) + require.NoError(t, err, "Error compacting JSON string") + return buffer.String() +} + +// ErrorMapsEqual is a testing method that takes any two maps of keys to errors and asserts that they have the same +// sets of keys, and that each associated error value has the same rendered message. +func ErrorMapsEqual[K comparable](t *testing.T, expected map[K]error, actual map[K]error) { + require.Equal(t, len(expected), len(actual)) + for key, expectedError := range expected { + error, ok := actual[key] + require.True(t, ok) + require.EqualError(t, error, expectedError.Error()) + } +} diff --git a/x/oracle/client/cli/query.go b/x/oracle/client/cli/query.go index 01a5f26cb..fe9db3a28 100644 --- a/x/oracle/client/cli/query.go +++ b/x/oracle/client/cli/query.go @@ -37,6 +37,8 @@ func GetQueryCmd(queryRoute string) *cobra.Command { cmd.AddCommand(CmdGetTimeBasedRewards()) + cmd.AddCommand(CmdGetMedianValues()) + // this line is used by starport scaffolding # 1 return cmd diff --git a/x/oracle/client/cli/query_get_median_values.go b/x/oracle/client/cli/query_get_median_values.go new file mode 100644 index 000000000..81d33cf87 --- /dev/null +++ b/x/oracle/client/cli/query_get_median_values.go @@ -0,0 +1,42 @@ +package cli + +import ( + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/cobra" + "github.com/tellor-io/layer/x/oracle/types" +) + +var _ = strconv.Itoa(0) + +func CmdGetMedianValues() *cobra.Command { + cmd := &cobra.Command{ + Use: "get-median-values", + Short: "Query getMedianValues", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) (err error) { + + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + queryClient := types.NewQueryClient(clientCtx) + + params := &types.QueryGetMedianValuesRequest{} + + res, err := queryClient.GetMedianValues(cmd.Context(), params) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 19d328699..06500ae4b 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -9,21 +9,23 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" - + daemontypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricefeedtypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" "github.com/tellor-io/layer/x/oracle/types" ) type ( Keeper struct { - cdc codec.BinaryCodec - storeKey storetypes.StoreKey - memKey storetypes.StoreKey - - accountKeeper types.AccountKeeper - bankKeeper types.BankKeeper - distrKeeper types.DistrKeeper - stakingKeeper types.StakingKeeper - registryKeeper types.RegistryKeeper + cdc codec.BinaryCodec + storeKey storetypes.StoreKey + memKey storetypes.StoreKey + marketParamConfig []daemontypes.MarketParam + indexPriceCache *pricefeedtypes.MarketToExchangePrices + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + distrKeeper types.DistrKeeper + stakingKeeper types.StakingKeeper + registryKeeper types.RegistryKeeper // the address capable of executing a MsgUpdateParams message. Typically, this // should be the x/gov module account. authority string @@ -34,7 +36,8 @@ func NewKeeper( cdc codec.BinaryCodec, storeKey, memKey storetypes.StoreKey, - + marketParamConfig []daemontypes.MarketParam, + indexPriceCache *pricefeedtypes.MarketToExchangePrices, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, distrKeeper types.DistrKeeper, @@ -47,16 +50,17 @@ func NewKeeper( } return &Keeper{ - cdc: cdc, - storeKey: storeKey, - memKey: memKey, - - accountKeeper: accountKeeper, - bankKeeper: bankKeeper, - distrKeeper: distrKeeper, - stakingKeeper: stakingKeeper, - registryKeeper: registryKeeper, - authority: authority, + cdc: cdc, + storeKey: storeKey, + memKey: memKey, + marketParamConfig: marketParamConfig, + indexPriceCache: indexPriceCache, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + distrKeeper: distrKeeper, + stakingKeeper: stakingKeeper, + registryKeeper: registryKeeper, + authority: authority, } } diff --git a/x/oracle/keeper/keeper_test.go b/x/oracle/keeper/keeper_test.go index 690253ad3..36524c7e5 100644 --- a/x/oracle/keeper/keeper_test.go +++ b/x/oracle/keeper/keeper_test.go @@ -89,6 +89,8 @@ func (s *KeeperTestSuite) SetupTest() { cdc, storeKey, memStoreKey, + nil, + nil, s.accountKeeper, nil, s.distrKeeper, diff --git a/x/oracle/keeper/query_get_median_values.go b/x/oracle/keeper/query_get_median_values.go new file mode 100644 index 000000000..d866cd488 --- /dev/null +++ b/x/oracle/keeper/query_get_median_values.go @@ -0,0 +1,32 @@ +package keeper + +import ( + "context" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tellor-io/layer/x/oracle/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (k Keeper) GetMedianValues(goCtx context.Context, req *types.QueryGetMedianValuesRequest) (*types.QueryGetMedianValuesResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + ctx := sdk.UnwrapSDKContext(goCtx) + + // TODO: Process the query + _ = ctx + + values := k.indexPriceCache.GetValidMedianPrices(k.marketParamConfig, time.Now()) + medianValues := make([]*types.MedianValues, 0, len(values)) + for i, value := range values { + medianValues = append(medianValues, &types.MedianValues{ + MarketId: i, + Price: value, + }) + } + return &types.QueryGetMedianValuesResponse{MedianValues: medianValues}, nil +} diff --git a/x/oracle/module.go b/x/oracle/module.go index b52f80167..2bc8224d2 100644 --- a/x/oracle/module.go +++ b/x/oracle/module.go @@ -201,6 +201,8 @@ func ProvideModule(in OracleInputs) OracleOutputs { in.Cdc, in.KvStoreKey, in.MemStoreKey, + nil, + nil, in.AccountKeeper, in.BankKeeper, in.Distr, diff --git a/x/oracle/types/median_values.pb.go b/x/oracle/types/median_values.pb.go new file mode 100644 index 000000000..d42c6dbef --- /dev/null +++ b/x/oracle/types/median_values.pb.go @@ -0,0 +1,335 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: layer/oracle/median_values.proto + +package types + +import ( + fmt "fmt" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type MedianValues struct { + MarketId uint32 `protobuf:"varint,1,opt,name=marketId,proto3" json:"marketId,omitempty"` + Price uint64 `protobuf:"varint,2,opt,name=price,proto3" json:"price,omitempty"` +} + +func (m *MedianValues) Reset() { *m = MedianValues{} } +func (m *MedianValues) String() string { return proto.CompactTextString(m) } +func (*MedianValues) ProtoMessage() {} +func (*MedianValues) Descriptor() ([]byte, []int) { + return fileDescriptor_5100164e71c118d7, []int{0} +} +func (m *MedianValues) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MedianValues) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MedianValues.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MedianValues) XXX_Merge(src proto.Message) { + xxx_messageInfo_MedianValues.Merge(m, src) +} +func (m *MedianValues) XXX_Size() int { + return m.Size() +} +func (m *MedianValues) XXX_DiscardUnknown() { + xxx_messageInfo_MedianValues.DiscardUnknown(m) +} + +var xxx_messageInfo_MedianValues proto.InternalMessageInfo + +func (m *MedianValues) GetMarketId() uint32 { + if m != nil { + return m.MarketId + } + return 0 +} + +func (m *MedianValues) GetPrice() uint64 { + if m != nil { + return m.Price + } + return 0 +} + +func init() { + proto.RegisterType((*MedianValues)(nil), "layer.oracle.MedianValues") +} + +func init() { proto.RegisterFile("layer/oracle/median_values.proto", fileDescriptor_5100164e71c118d7) } + +var fileDescriptor_5100164e71c118d7 = []byte{ + // 180 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xc8, 0x49, 0xac, 0x4c, + 0x2d, 0xd2, 0xcf, 0x2f, 0x4a, 0x4c, 0xce, 0x49, 0xd5, 0xcf, 0x4d, 0x4d, 0xc9, 0x4c, 0xcc, 0x8b, + 0x2f, 0x4b, 0xcc, 0x29, 0x4d, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0xab, + 0xd0, 0x83, 0xa8, 0x50, 0x72, 0xe0, 0xe2, 0xf1, 0x05, 0x2b, 0x0a, 0x03, 0xab, 0x11, 0x92, 0xe2, + 0xe2, 0xc8, 0x4d, 0x2c, 0xca, 0x4e, 0x2d, 0xf1, 0x4c, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0d, + 0x82, 0xf3, 0x85, 0x44, 0xb8, 0x58, 0x0b, 0x8a, 0x32, 0x93, 0x53, 0x25, 0x98, 0x14, 0x18, 0x35, + 0x58, 0x82, 0x20, 0x1c, 0x27, 0xe7, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, + 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, 0x6e, 0x3c, 0x96, 0x63, 0x88, + 0xd2, 0x4c, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x2f, 0x49, 0xcd, 0xc9, + 0xc9, 0x2f, 0xd2, 0xcd, 0xcc, 0xd7, 0x87, 0x38, 0xb0, 0x02, 0xe6, 0xc4, 0x92, 0xca, 0x82, 0xd4, + 0xe2, 0x24, 0x36, 0xb0, 0xdb, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x88, 0x84, 0xe1, 0x9d, + 0xbf, 0x00, 0x00, 0x00, +} + +func (m *MedianValues) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MedianValues) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MedianValues) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Price != 0 { + i = encodeVarintMedianValues(dAtA, i, uint64(m.Price)) + i-- + dAtA[i] = 0x10 + } + if m.MarketId != 0 { + i = encodeVarintMedianValues(dAtA, i, uint64(m.MarketId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintMedianValues(dAtA []byte, offset int, v uint64) int { + offset -= sovMedianValues(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MedianValues) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.MarketId != 0 { + n += 1 + sovMedianValues(uint64(m.MarketId)) + } + if m.Price != 0 { + n += 1 + sovMedianValues(uint64(m.Price)) + } + return n +} + +func sovMedianValues(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozMedianValues(x uint64) (n int) { + return sovMedianValues(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MedianValues) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MedianValues: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MedianValues: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MarketId", wireType) + } + m.MarketId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MarketId |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) + } + m.Price = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Price |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMedianValues(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMedianValues + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipMedianValues(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMedianValues + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMedianValues + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMedianValues + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthMedianValues + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupMedianValues + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthMedianValues + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthMedianValues = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowMedianValues = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupMedianValues = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/oracle/types/query.pb.go b/x/oracle/types/query.pb.go index e1fd3cff9..e6b19a52c 100644 --- a/x/oracle/types/query.pb.go +++ b/x/oracle/types/query.pb.go @@ -749,6 +749,86 @@ func (m *QueryGetTimeBasedRewardsResponse) GetReward() types.Coin { return types.Coin{} } +type QueryGetMedianValuesRequest struct { +} + +func (m *QueryGetMedianValuesRequest) Reset() { *m = QueryGetMedianValuesRequest{} } +func (m *QueryGetMedianValuesRequest) String() string { return proto.CompactTextString(m) } +func (*QueryGetMedianValuesRequest) ProtoMessage() {} +func (*QueryGetMedianValuesRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_bb27caa9d264c5c4, []int{16} +} +func (m *QueryGetMedianValuesRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryGetMedianValuesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryGetMedianValuesRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryGetMedianValuesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryGetMedianValuesRequest.Merge(m, src) +} +func (m *QueryGetMedianValuesRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryGetMedianValuesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryGetMedianValuesRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryGetMedianValuesRequest proto.InternalMessageInfo + +type QueryGetMedianValuesResponse struct { + MedianValues []*MedianValues `protobuf:"bytes,1,rep,name=medianValues,proto3" json:"medianValues,omitempty"` +} + +func (m *QueryGetMedianValuesResponse) Reset() { *m = QueryGetMedianValuesResponse{} } +func (m *QueryGetMedianValuesResponse) String() string { return proto.CompactTextString(m) } +func (*QueryGetMedianValuesResponse) ProtoMessage() {} +func (*QueryGetMedianValuesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_bb27caa9d264c5c4, []int{17} +} +func (m *QueryGetMedianValuesResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryGetMedianValuesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryGetMedianValuesResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryGetMedianValuesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryGetMedianValuesResponse.Merge(m, src) +} +func (m *QueryGetMedianValuesResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryGetMedianValuesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryGetMedianValuesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryGetMedianValuesResponse proto.InternalMessageInfo + +func (m *QueryGetMedianValuesResponse) GetMedianValues() []*MedianValues { + if m != nil { + return m.MedianValues + } + return nil +} + func init() { proto.RegisterType((*QueryParamsRequest)(nil), "layer.oracle.QueryParamsRequest") proto.RegisterType((*QueryParamsResponse)(nil), "layer.oracle.QueryParamsResponse") @@ -766,77 +846,84 @@ func init() { proto.RegisterType((*QueryGetDataBeforeRequest)(nil), "layer.oracle.QueryGetDataBeforeRequest") proto.RegisterType((*QueryGetTimeBasedRewardsRequest)(nil), "layer.oracle.QueryGetTimeBasedRewardsRequest") proto.RegisterType((*QueryGetTimeBasedRewardsResponse)(nil), "layer.oracle.QueryGetTimeBasedRewardsResponse") + proto.RegisterType((*QueryGetMedianValuesRequest)(nil), "layer.oracle.QueryGetMedianValuesRequest") + proto.RegisterType((*QueryGetMedianValuesResponse)(nil), "layer.oracle.QueryGetMedianValuesResponse") } func init() { proto.RegisterFile("layer/oracle/query.proto", fileDescriptor_bb27caa9d264c5c4) } var fileDescriptor_bb27caa9d264c5c4 = []byte{ - // 1036 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x96, 0xcf, 0x6f, 0x1b, 0x45, - 0x14, 0xc7, 0xb3, 0x69, 0x71, 0xc9, 0xa4, 0x08, 0x31, 0x35, 0x4d, 0xb2, 0x8d, 0x9c, 0x64, 0x91, - 0xda, 0x24, 0x28, 0x3b, 0x38, 0x6d, 0x09, 0x55, 0x55, 0xa2, 0xda, 0x45, 0x55, 0x0f, 0x48, 0xd4, - 0x71, 0x25, 0x04, 0x07, 0x6b, 0x6c, 0x0f, 0xdb, 0x51, 0x6d, 0xcf, 0x66, 0x77, 0x0c, 0x58, 0x91, - 0x2f, 0x5c, 0xb9, 0x20, 0x71, 0xe4, 0xc0, 0x15, 0x21, 0x90, 0xca, 0x1d, 0x89, 0x6b, 0x8f, 0x91, - 0xb8, 0x70, 0x02, 0x94, 0x20, 0xf1, 0x47, 0x70, 0x41, 0x33, 0xf3, 0xd6, 0xeb, 0xb1, 0x77, 0x9d, - 0xc0, 0x25, 0xd9, 0x9d, 0xf7, 0x63, 0x3f, 0xef, 0xcd, 0xf3, 0x77, 0x06, 0x2d, 0x77, 0xe8, 0x80, - 0x45, 0x44, 0x44, 0xb4, 0xd5, 0x61, 0xe4, 0xb0, 0xcf, 0xa2, 0x81, 0x1f, 0x46, 0x42, 0x0a, 0x7c, - 0x59, 0x5b, 0x7c, 0x63, 0x71, 0x8b, 0x81, 0x08, 0x84, 0x36, 0x10, 0xf5, 0x64, 0x7c, 0xdc, 0xd5, - 0x40, 0x88, 0xa0, 0xc3, 0x08, 0x0d, 0x39, 0xa1, 0xbd, 0x9e, 0x90, 0x54, 0x72, 0xd1, 0x8b, 0xc1, - 0xba, 0xdd, 0x12, 0x71, 0x57, 0xc4, 0xa4, 0x49, 0x63, 0x48, 0x4d, 0x3e, 0x2d, 0x37, 0x99, 0xa4, - 0x65, 0x12, 0xd2, 0x80, 0xf7, 0xb4, 0x33, 0xf8, 0xae, 0x58, 0x1c, 0x21, 0x8d, 0x68, 0x37, 0x49, - 0xe3, 0x5a, 0xa6, 0x88, 0x85, 0x22, 0x92, 0x89, 0x6d, 0xcd, 0xb2, 0x75, 0x79, 0x2b, 0x12, 0x0d, - 0xe3, 0x01, 0x0e, 0x4b, 0x96, 0x83, 0xe4, 0x61, 0x12, 0x79, 0xcd, 0x32, 0xf4, 0x63, 0x16, 0x35, - 0x24, 0x0f, 0x93, 0xba, 0x2c, 0x23, 0x0d, 0x82, 0x88, 0x05, 0x54, 0x32, 0xb0, 0xbe, 0x46, 0xbb, - 0xbc, 0x27, 0x88, 0xfe, 0x0b, 0x4b, 0xa5, 0xf1, 0x52, 0x93, 0x22, 0x5b, 0x82, 0x43, 0x79, 0x5e, - 0x11, 0xe1, 0xc7, 0xaa, 0x01, 0x1f, 0xe8, 0xc2, 0x6a, 0xec, 0xb0, 0xcf, 0x62, 0xe9, 0x3d, 0x42, - 0x57, 0xac, 0xd5, 0x38, 0x14, 0xbd, 0x98, 0xe1, 0x5d, 0x54, 0x30, 0x0d, 0x58, 0x76, 0xd6, 0x9d, - 0xcd, 0xc5, 0xdd, 0xa2, 0x3f, 0xbe, 0x15, 0xbe, 0xf1, 0xae, 0x5c, 0x7c, 0xf1, 0xfb, 0xda, 0x5c, - 0x0d, 0x3c, 0xbd, 0x3d, 0x74, 0x4d, 0xa7, 0x7a, 0xc8, 0x64, 0xcd, 0x74, 0xa8, 0x39, 0x78, 0xcc, - 0xdb, 0xf0, 0x25, 0xbc, 0x8c, 0x2e, 0xe9, 0x0d, 0x78, 0xd4, 0xd6, 0x39, 0x17, 0x6a, 0xc9, 0xab, - 0xf7, 0x04, 0xad, 0x66, 0x07, 0x02, 0xcc, 0x6d, 0x74, 0x09, 0x5a, 0x0e, 0x34, 0xaf, 0xdb, 0x34, - 0x10, 0x04, 0x38, 0x89, 0xaf, 0xf7, 0x2e, 0x5a, 0x9f, 0x4a, 0x6b, 0x1e, 0x58, 0x94, 0x40, 0xb9, - 0xe8, 0xe5, 0x08, 0x96, 0x80, 0x6a, 0xf4, 0xee, 0x3d, 0x45, 0x1b, 0x33, 0xe2, 0x81, 0xad, 0x8a, - 0x2e, 0xeb, 0x2d, 0xaf, 0x8d, 0x00, 0x2f, 0x6c, 0x2e, 0xee, 0xae, 0xd8, 0x80, 0xef, 0xa7, 0x1e, - 0x00, 0x69, 0x05, 0x79, 0x1f, 0xa3, 0x37, 0x72, 0xbf, 0x34, 0xd6, 0xc1, 0x19, 0xb0, 0xe3, 0xdd, - 0x9d, 0xb7, 0xbb, 0x7b, 0x07, 0xad, 0x24, 0xc9, 0xab, 0xfd, 0x28, 0x62, 0x3d, 0x59, 0xe7, 0x61, - 0x92, 0x72, 0x15, 0x2d, 0x68, 0xbf, 0x07, 0x54, 0x52, 0xc8, 0x99, 0x2e, 0x78, 0x0f, 0x90, 0x9b, - 0x15, 0x0a, 0xa5, 0x5f, 0x47, 0x17, 0xd5, 0x30, 0xc3, 0x9e, 0x60, 0xbb, 0xe4, 0x3a, 0x0f, 0xe3, - 0x9a, 0xb6, 0x7b, 0x07, 0xe9, 0x5c, 0x3c, 0x89, 0x59, 0x54, 0xe7, 0x61, 0x5d, 0x48, 0xda, 0x49, - 0x10, 0xae, 0xa2, 0x82, 0xe4, 0x61, 0x38, 0xaa, 0x09, 0xde, 0x6c, 0xb4, 0xf9, 0x49, 0xb4, 0x0f, - 0xd3, 0x99, 0xb1, 0x93, 0x02, 0xdc, 0x3b, 0x68, 0x41, 0xaa, 0x85, 0x7a, 0x4a, 0xe8, 0xda, 0x84, - 0x56, 0x58, 0xea, 0xec, 0x55, 0xd0, 0xf5, 0x89, 0xa2, 0xef, 0x27, 0x3f, 0xbe, 0xb6, 0xd9, 0x94, - 0xb3, 0x27, 0xfa, 0x20, 0x1d, 0xbd, 0xe9, 0x60, 0x20, 0x24, 0xa8, 0x60, 0x76, 0x0f, 0xf0, 0x96, - 0x6c, 0xbc, 0x51, 0x5c, 0x0d, 0xdc, 0xbc, 0x83, 0x74, 0x23, 0x55, 0x0b, 0x2a, 0xec, 0x13, 0x11, - 0xb1, 0x33, 0x59, 0x54, 0x1f, 0x25, 0xef, 0xb2, 0x58, 0xd2, 0x6e, 0xa8, 0xfb, 0x78, 0xa1, 0x96, - 0x2e, 0x78, 0x1b, 0x68, 0x2d, 0x49, 0x5a, 0xe7, 0x5d, 0x56, 0xa1, 0xb1, 0x02, 0xfd, 0x8c, 0x46, - 0xed, 0x91, 0x44, 0x7c, 0xe9, 0xa4, 0xd5, 0x4c, 0xfb, 0x40, 0x35, 0x81, 0xaa, 0x46, 0x2d, 0x41, - 0x35, 0x2b, 0xbe, 0x91, 0x23, 0x5f, 0xc9, 0x91, 0x0f, 0x72, 0xe4, 0x57, 0x05, 0xef, 0x55, 0x6e, - 0xa9, 0x5f, 0xc0, 0xf7, 0x7f, 0xac, 0xdd, 0x08, 0xb8, 0x7c, 0xda, 0x6f, 0xfa, 0x2d, 0xd1, 0x25, - 0xa0, 0x5d, 0xe6, 0xdf, 0x4e, 0xdc, 0x7e, 0x46, 0xe4, 0x20, 0x64, 0xb1, 0x0e, 0xf8, 0xee, 0xef, - 0xe7, 0xdb, 0x4e, 0x0d, 0xd2, 0xef, 0xfe, 0xb3, 0x88, 0x5e, 0xd2, 0x34, 0xf8, 0x19, 0x2a, 0x18, - 0x1d, 0xc2, 0xeb, 0x76, 0xeb, 0xa6, 0x65, 0xce, 0xdd, 0x98, 0xe1, 0x61, 0x2a, 0xf0, 0x56, 0xbf, - 0xf8, 0xf5, 0xaf, 0xaf, 0xe7, 0xaf, 0xe2, 0x22, 0xc9, 0x38, 0x07, 0xf0, 0x37, 0x0e, 0x7a, 0x75, - 0x42, 0x9f, 0xf0, 0x56, 0x46, 0xd2, 0x6c, 0xf1, 0x73, 0xb7, 0xcf, 0xe3, 0x0a, 0x20, 0x65, 0x0d, - 0xf2, 0x26, 0xde, 0xb2, 0x41, 0x02, 0x26, 0xe1, 0x5c, 0x89, 0x9b, 0x83, 0xc6, 0x21, 0x6f, 0x93, - 0x23, 0xd8, 0xe2, 0x21, 0x7e, 0xee, 0xa0, 0x62, 0x96, 0x78, 0x60, 0xff, 0x8c, 0xef, 0x4e, 0xe8, - 0xa1, 0x4b, 0xce, 0xed, 0x0f, 0xb0, 0x7b, 0x1a, 0xb6, 0x8c, 0xc9, 0x2c, 0xd8, 0x44, 0xa5, 0xc8, - 0x51, 0xf2, 0x34, 0xc4, 0xbf, 0x38, 0x68, 0x29, 0x47, 0xef, 0x70, 0xf9, 0x9c, 0x14, 0xff, 0xb3, - 0xc1, 0xef, 0x69, 0xe6, 0x7d, 0x7c, 0xef, 0x3c, 0xcc, 0xa6, 0xd3, 0x23, 0xee, 0xb1, 0xa6, 0x7f, - 0xeb, 0xa0, 0x57, 0x2c, 0x65, 0xc4, 0x37, 0xb2, 0x21, 0xa6, 0x64, 0xd7, 0xdd, 0x3c, 0xdb, 0x11, - 0x58, 0xef, 0x6a, 0xd6, 0xdb, 0xf8, 0x26, 0x91, 0xac, 0xd3, 0x11, 0xd1, 0x0e, 0x17, 0xd3, 0xd4, - 0x2d, 0x13, 0xa7, 0x6e, 0x0f, 0xc0, 0xa7, 0x04, 0x62, 0x88, 0x7f, 0x32, 0x43, 0x3b, 0xae, 0x74, - 0x79, 0x43, 0x9b, 0xa1, 0xcc, 0x79, 0x3d, 0xcd, 0xd2, 0x5b, 0xef, 0xa1, 0xe6, 0xbc, 0x8f, 0xf7, - 0x67, 0x71, 0x26, 0x57, 0x9c, 0x86, 0x56, 0x5b, 0x72, 0x64, 0x94, 0x7e, 0x68, 0x31, 0xff, 0xec, - 0xa0, 0x2b, 0x19, 0xb2, 0x89, 0x6f, 0xcd, 0x6c, 0x59, 0x8e, 0x44, 0xbb, 0x39, 0xf3, 0x9f, 0x27, - 0xca, 0xde, 0xbe, 0x2e, 0xe3, 0x0e, 0xde, 0x9b, 0x55, 0xc6, 0xe8, 0x32, 0xd6, 0x86, 0x29, 0x19, - 0x1b, 0x8a, 0x1f, 0xcd, 0x50, 0xa4, 0x02, 0x9d, 0x37, 0x14, 0x53, 0x12, 0xfe, 0x9f, 0x59, 0xab, - 0x9a, 0xf5, 0x1e, 0xbe, 0x9b, 0xc7, 0x6a, 0x7d, 0x25, 0x65, 0x54, 0x7d, 0x07, 0xf9, 0x1f, 0xe2, - 0x1f, 0x4c, 0xbb, 0x27, 0x75, 0x1d, 0xef, 0x64, 0xc3, 0xe4, 0x9c, 0x11, 0x79, 0xec, 0x79, 0xc7, - 0x85, 0xf7, 0xb6, 0x66, 0x7f, 0x0b, 0xfb, 0xb3, 0xfa, 0xac, 0x30, 0x1b, 0xea, 0xfc, 0x50, 0x7d, - 0xd6, 0xf1, 0x95, 0xea, 0x8b, 0x93, 0x92, 0x73, 0x7c, 0x52, 0x72, 0xfe, 0x3c, 0x29, 0x39, 0x5f, - 0x9d, 0x96, 0xe6, 0x8e, 0x4f, 0x4b, 0x73, 0xbf, 0x9d, 0x96, 0xe6, 0x3e, 0xda, 0x1a, 0x3b, 0x4d, - 0x26, 0x73, 0x7e, 0x3e, 0xba, 0x7c, 0xab, 0x43, 0xa5, 0x59, 0xd0, 0x17, 0xe2, 0x9b, 0xff, 0x06, - 0x00, 0x00, 0xff, 0xff, 0x8c, 0xe6, 0x8a, 0x07, 0x79, 0x0c, 0x00, 0x00, + // 1108 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x97, 0x41, 0x6f, 0x1b, 0x45, + 0x14, 0xc7, 0xb3, 0x69, 0x71, 0xc9, 0x34, 0x15, 0x62, 0x6a, 0x9a, 0x64, 0x1b, 0x9c, 0x64, 0x91, + 0xda, 0x24, 0x28, 0x3b, 0x38, 0x6d, 0x09, 0x55, 0xd5, 0x46, 0xb5, 0x8b, 0xaa, 0x1e, 0x2a, 0x51, + 0xc7, 0x45, 0x08, 0x24, 0xac, 0xb1, 0x3d, 0x6c, 0x47, 0xb5, 0x3d, 0x9b, 0xdd, 0x71, 0xc1, 0x8a, + 0x7c, 0xe1, 0xca, 0x05, 0x89, 0x13, 0xe2, 0xc0, 0x15, 0x21, 0x90, 0xca, 0x1d, 0x89, 0x6b, 0x8f, + 0x91, 0xb8, 0x70, 0x02, 0x94, 0x20, 0xf1, 0x35, 0xd0, 0xce, 0xbc, 0xf5, 0xee, 0xd8, 0xbb, 0x4e, + 0xe8, 0x25, 0xd9, 0x9d, 0x79, 0xef, 0xed, 0xef, 0xbd, 0x79, 0xfb, 0x7f, 0x6b, 0xb4, 0xd8, 0xa1, + 0x03, 0x16, 0x10, 0x11, 0xd0, 0x56, 0x87, 0x91, 0xfd, 0x3e, 0x0b, 0x06, 0xae, 0x1f, 0x08, 0x29, + 0xf0, 0xbc, 0xda, 0x71, 0xf5, 0x8e, 0x5d, 0xf4, 0x84, 0x27, 0xd4, 0x06, 0x89, 0xae, 0xb4, 0x8d, + 0xbd, 0xec, 0x09, 0xe1, 0x75, 0x18, 0xa1, 0x3e, 0x27, 0xb4, 0xd7, 0x13, 0x92, 0x4a, 0x2e, 0x7a, + 0x21, 0xec, 0x6e, 0xb6, 0x44, 0xd8, 0x15, 0x21, 0x69, 0xd2, 0x10, 0x42, 0x93, 0x67, 0xe5, 0x26, + 0x93, 0xb4, 0x4c, 0x7c, 0xea, 0xf1, 0x9e, 0x32, 0x06, 0xdb, 0x25, 0x83, 0xc3, 0xa7, 0x01, 0xed, + 0xc6, 0x61, 0x6c, 0x63, 0x2b, 0x60, 0xbe, 0x08, 0x64, 0xbc, 0xb7, 0x62, 0xec, 0x75, 0x79, 0x2b, + 0x10, 0x0d, 0x6d, 0x01, 0x06, 0x0b, 0x86, 0x81, 0xe4, 0x7e, 0xec, 0x79, 0xd9, 0xd8, 0xe8, 0x87, + 0x2c, 0x68, 0x48, 0xee, 0xc7, 0x79, 0x19, 0x9b, 0xd4, 0xf3, 0x02, 0xe6, 0x51, 0xc9, 0x60, 0xf7, + 0x75, 0xda, 0xe5, 0x3d, 0x41, 0xd4, 0x5f, 0x58, 0x2a, 0xa5, 0x53, 0x8d, 0x93, 0x6c, 0x09, 0x1e, + 0xa7, 0xb7, 0x6a, 0x72, 0xb2, 0x36, 0xa7, 0xbd, 0xc6, 0x33, 0xda, 0xe9, 0x33, 0xe0, 0x71, 0x8a, + 0x08, 0x3f, 0x8a, 0x4a, 0xf4, 0x81, 0x4a, 0xbd, 0xc6, 0xf6, 0xfb, 0x2c, 0x94, 0xce, 0x03, 0x74, + 0xd1, 0x58, 0x0d, 0x7d, 0xd1, 0x0b, 0x19, 0xde, 0x46, 0x05, 0x5d, 0xa2, 0x45, 0x6b, 0xd5, 0x5a, + 0x3f, 0xbf, 0x5d, 0x74, 0xd3, 0x87, 0xe5, 0x6a, 0xeb, 0xca, 0xd9, 0x17, 0x7f, 0xae, 0xcc, 0xd4, + 0xc0, 0xd2, 0xd9, 0x41, 0x97, 0x55, 0xa8, 0xfb, 0x4c, 0xd6, 0x74, 0x0d, 0x9b, 0x83, 0x47, 0xbc, + 0x0d, 0x4f, 0xc2, 0x8b, 0xe8, 0x9c, 0x3a, 0xa2, 0x07, 0x6d, 0x15, 0x73, 0xae, 0x16, 0xdf, 0x3a, + 0x8f, 0xd1, 0x72, 0xb6, 0x23, 0xc0, 0xdc, 0x40, 0xe7, 0xe0, 0x50, 0x80, 0xe6, 0x0d, 0x93, 0x06, + 0x9c, 0x00, 0x27, 0xb6, 0x75, 0xee, 0xa0, 0xd5, 0x89, 0xb0, 0xfa, 0x82, 0x05, 0x31, 0x94, 0x8d, + 0x5e, 0x0d, 0x60, 0x09, 0xa8, 0x46, 0xf7, 0xce, 0x13, 0xb4, 0x36, 0xc5, 0x1f, 0xd8, 0xaa, 0x68, + 0x5e, 0x35, 0x45, 0x6d, 0x04, 0x78, 0x66, 0xfd, 0xfc, 0xf6, 0x92, 0x09, 0xf8, 0x30, 0xb1, 0x00, + 0x48, 0xc3, 0xc9, 0xf9, 0x04, 0xbd, 0x95, 0xfb, 0xa4, 0x54, 0x05, 0xa7, 0xc0, 0xa6, 0xab, 0x3b, + 0x6b, 0x56, 0xf7, 0x26, 0x5a, 0x8a, 0x83, 0x57, 0xfb, 0x41, 0xc0, 0x7a, 0xb2, 0xce, 0xfd, 0x38, + 0xe4, 0x32, 0x9a, 0x53, 0x76, 0xf7, 0xa8, 0xa4, 0x10, 0x33, 0x59, 0x70, 0xee, 0x21, 0x3b, 0xcb, + 0x15, 0x52, 0xbf, 0x82, 0xce, 0x46, 0xed, 0x0e, 0x67, 0x82, 0xcd, 0x94, 0xeb, 0xdc, 0x0f, 0x6b, + 0x6a, 0xdf, 0xd9, 0x4b, 0xfa, 0xe2, 0x71, 0xc8, 0x82, 0x3a, 0xf7, 0xeb, 0x42, 0xd2, 0x4e, 0x8c, + 0x70, 0x09, 0x15, 0x24, 0xf7, 0xfd, 0x51, 0x4e, 0x70, 0x67, 0xa2, 0xcd, 0x8e, 0xa3, 0x7d, 0x94, + 0xf4, 0x8c, 0x19, 0x14, 0xe0, 0xde, 0x43, 0x73, 0x32, 0x5a, 0xa8, 0x27, 0x84, 0xb6, 0x49, 0x68, + 0xb8, 0x25, 0xc6, 0x4e, 0x05, 0x5d, 0x19, 0x4b, 0xfa, 0x6e, 0xfc, 0x7a, 0xb6, 0xf5, 0xa1, 0x9c, + 0xdc, 0xd1, 0x7b, 0x49, 0xeb, 0x4d, 0x3a, 0x03, 0x21, 0x41, 0x05, 0x7d, 0x7a, 0x80, 0xb7, 0x60, + 0xe2, 0x8d, 0xfc, 0x6a, 0x60, 0xe6, 0xec, 0x25, 0x07, 0x19, 0x95, 0xa0, 0xc2, 0x3e, 0x13, 0x01, + 0x3b, 0x91, 0x25, 0xaa, 0xa3, 0xe4, 0x5d, 0x16, 0x4a, 0xda, 0xf5, 0x55, 0x1d, 0xcf, 0xd4, 0x92, + 0x05, 0x67, 0x0d, 0xad, 0xc4, 0x41, 0xeb, 0xbc, 0xcb, 0x2a, 0x34, 0x8c, 0x40, 0x3f, 0xa7, 0x41, + 0x7b, 0x24, 0x11, 0x5f, 0x59, 0x49, 0x36, 0x93, 0x36, 0x90, 0x8d, 0x17, 0x65, 0x13, 0x2d, 0x41, + 0x36, 0x4b, 0xae, 0x16, 0x2c, 0x37, 0x12, 0x2c, 0x17, 0x04, 0xcb, 0xad, 0x0a, 0xde, 0xab, 0x5c, + 0x8f, 0xde, 0x80, 0x1f, 0xff, 0x5a, 0xb9, 0xea, 0x71, 0xf9, 0xa4, 0xdf, 0x74, 0x5b, 0xa2, 0x4b, + 0x40, 0xdd, 0xf4, 0xbf, 0xad, 0xb0, 0xfd, 0x94, 0xc8, 0x81, 0xcf, 0x42, 0xe5, 0xf0, 0xc3, 0xbf, + 0xcf, 0x37, 0xad, 0x1a, 0x84, 0x77, 0xde, 0x4c, 0xba, 0xe9, 0xa1, 0x52, 0xb9, 0x0f, 0x95, 0xc8, + 0xc5, 0xb0, 0x9f, 0x26, 0x7d, 0x61, 0x6e, 0x03, 0xe7, 0x1d, 0x34, 0xdf, 0x4d, 0xad, 0xc3, 0xfb, + 0x3a, 0xd6, 0x1a, 0x86, 0xa7, 0x61, 0xbf, 0xfd, 0xed, 0x05, 0xf4, 0x8a, 0x7a, 0x00, 0x7e, 0x8a, + 0x0a, 0x5a, 0x06, 0xf1, 0xaa, 0xe9, 0x3d, 0xa9, 0xb2, 0xf6, 0xda, 0x14, 0x0b, 0x0d, 0xe6, 0x2c, + 0x7f, 0xf9, 0xfb, 0x3f, 0xdf, 0xcc, 0x5e, 0xc2, 0x45, 0x92, 0x31, 0xa8, 0xf0, 0x77, 0x16, 0x7a, + 0x6d, 0x4c, 0x1e, 0xf1, 0x46, 0x46, 0xd0, 0x6c, 0xed, 0xb5, 0x37, 0x4f, 0x63, 0x0a, 0x20, 0x65, + 0x05, 0xf2, 0x36, 0xde, 0x30, 0x41, 0x3c, 0x26, 0x61, 0xf0, 0x85, 0xcd, 0x41, 0x63, 0x9f, 0xb7, + 0xc9, 0x01, 0x74, 0xd8, 0x10, 0x3f, 0xb7, 0x50, 0x31, 0x4b, 0xbb, 0xb0, 0x7b, 0xc2, 0x73, 0xc7, + 0xe4, 0xd8, 0x26, 0xa7, 0xb6, 0x07, 0xd8, 0x1d, 0x05, 0x5b, 0xc6, 0x64, 0x1a, 0x6c, 0x2c, 0x92, + 0xe4, 0x20, 0xbe, 0x1a, 0xe2, 0xdf, 0x2c, 0xb4, 0x90, 0x23, 0xb7, 0xb8, 0x7c, 0x4a, 0x8a, 0x97, + 0x2c, 0xf0, 0xfb, 0x8a, 0x79, 0x17, 0xdf, 0x3e, 0x0d, 0xb3, 0xae, 0xf4, 0x88, 0x3b, 0x55, 0xf4, + 0xef, 0x2d, 0x74, 0xc1, 0x10, 0x66, 0x7c, 0x35, 0x1b, 0x62, 0x42, 0xf5, 0xed, 0xf5, 0x93, 0x0d, + 0x81, 0xf5, 0x96, 0x62, 0xbd, 0x81, 0xaf, 0x11, 0xc9, 0x3a, 0x1d, 0x11, 0x6c, 0x71, 0x31, 0x49, + 0xdd, 0xd2, 0x7e, 0xd1, 0xe7, 0x0d, 0xf0, 0x45, 0xfa, 0x34, 0xc4, 0xbf, 0xe8, 0xa6, 0x4d, 0x0b, + 0x6d, 0x5e, 0xd3, 0x66, 0x0c, 0x86, 0xbc, 0x9a, 0x66, 0xc9, 0xbd, 0x73, 0x5f, 0x71, 0xde, 0xc5, + 0xbb, 0xd3, 0x38, 0xe3, 0x6f, 0xb0, 0x86, 0x12, 0x7b, 0x72, 0xa0, 0x07, 0xcd, 0xd0, 0x60, 0xfe, + 0xd5, 0x42, 0x17, 0x33, 0x54, 0x1b, 0x5f, 0x9f, 0x5a, 0xb2, 0x9c, 0x09, 0x61, 0xe7, 0xf4, 0x7f, + 0xde, 0x4c, 0x70, 0x76, 0x55, 0x1a, 0x37, 0xf1, 0xce, 0xb4, 0x34, 0x46, 0x5f, 0x8b, 0x6d, 0xe8, + 0x92, 0x54, 0x53, 0xfc, 0xac, 0x9b, 0x22, 0x99, 0x0f, 0x79, 0x4d, 0x31, 0x31, 0x41, 0xfe, 0x37, + 0x6b, 0x55, 0xb1, 0xde, 0xc6, 0xb7, 0xf2, 0x58, 0x8d, 0xa7, 0x24, 0x8c, 0x51, 0xdd, 0x61, 0xfa, + 0x0c, 0xf1, 0x4f, 0xba, 0xdc, 0xe3, 0x63, 0x05, 0x6f, 0x65, 0xc3, 0xe4, 0x8c, 0xa8, 0x3c, 0xf6, + 0xbc, 0x69, 0xe5, 0xbc, 0xab, 0xd8, 0xdf, 0xc1, 0xee, 0xb4, 0x3a, 0x47, 0x98, 0x8d, 0x68, 0x7c, + 0x45, 0x75, 0xd6, 0x58, 0x20, 0xc3, 0xe9, 0xf9, 0x90, 0xd7, 0xd1, 0x19, 0xc3, 0x29, 0xaf, 0xa3, + 0xb3, 0x06, 0x55, 0x4a, 0x86, 0xa7, 0x20, 0x1a, 0xdf, 0xf9, 0x95, 0xea, 0x8b, 0xa3, 0x92, 0x75, + 0x78, 0x54, 0xb2, 0xfe, 0x3e, 0x2a, 0x59, 0x5f, 0x1f, 0x97, 0x66, 0x0e, 0x8f, 0x4b, 0x33, 0x7f, + 0x1c, 0x97, 0x66, 0x3e, 0xde, 0x48, 0x8d, 0xda, 0xf1, 0x70, 0x5f, 0x8c, 0x7e, 0xbb, 0x44, 0x13, + 0xb7, 0x59, 0x50, 0xbf, 0x16, 0xae, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0xe3, 0x75, 0x55, 0x5b, + 0xb8, 0x0d, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -867,6 +954,8 @@ type QueryClient interface { GetDataBefore(ctx context.Context, in *QueryGetDataBeforeRequest, opts ...grpc.CallOption) (*QueryGetAggregatedReportResponse, error) // Queries a list of GetTimeBasedRewards items. GetTimeBasedRewards(ctx context.Context, in *QueryGetTimeBasedRewardsRequest, opts ...grpc.CallOption) (*QueryGetTimeBasedRewardsResponse, error) + // Queries a list of GetMedianValues items. + GetMedianValues(ctx context.Context, in *QueryGetMedianValuesRequest, opts ...grpc.CallOption) (*QueryGetMedianValuesResponse, error) } type queryClient struct { @@ -958,6 +1047,15 @@ func (c *queryClient) GetTimeBasedRewards(ctx context.Context, in *QueryGetTimeB return out, nil } +func (c *queryClient) GetMedianValues(ctx context.Context, in *QueryGetMedianValuesRequest, opts ...grpc.CallOption) (*QueryGetMedianValuesResponse, error) { + out := new(QueryGetMedianValuesResponse) + err := c.cc.Invoke(ctx, "/layer.oracle.Query/GetMedianValues", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // QueryServer is the server API for Query service. type QueryServer interface { // Parameters queries the parameters of the module. @@ -976,6 +1074,8 @@ type QueryServer interface { GetDataBefore(context.Context, *QueryGetDataBeforeRequest) (*QueryGetAggregatedReportResponse, error) // Queries a list of GetTimeBasedRewards items. GetTimeBasedRewards(context.Context, *QueryGetTimeBasedRewardsRequest) (*QueryGetTimeBasedRewardsResponse, error) + // Queries a list of GetMedianValues items. + GetMedianValues(context.Context, *QueryGetMedianValuesRequest) (*QueryGetMedianValuesResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -1009,6 +1109,9 @@ func (*UnimplementedQueryServer) GetDataBefore(ctx context.Context, req *QueryGe func (*UnimplementedQueryServer) GetTimeBasedRewards(ctx context.Context, req *QueryGetTimeBasedRewardsRequest) (*QueryGetTimeBasedRewardsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetTimeBasedRewards not implemented") } +func (*UnimplementedQueryServer) GetMedianValues(ctx context.Context, req *QueryGetMedianValuesRequest) (*QueryGetMedianValuesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMedianValues not implemented") +} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -1176,6 +1279,24 @@ func _Query_GetTimeBasedRewards_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _Query_GetMedianValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryGetMedianValuesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).GetMedianValues(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.oracle.Query/GetMedianValues", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).GetMedianValues(ctx, req.(*QueryGetMedianValuesRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "layer.oracle.Query", HandlerType: (*QueryServer)(nil), @@ -1216,6 +1337,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "GetTimeBasedRewards", Handler: _Query_GetTimeBasedRewards_Handler, }, + { + MethodName: "GetMedianValues", + Handler: _Query_GetMedianValues_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "layer/oracle/query.proto", @@ -1737,6 +1862,66 @@ func (m *QueryGetTimeBasedRewardsResponse) MarshalToSizedBuffer(dAtA []byte) (in return len(dAtA) - i, nil } +func (m *QueryGetMedianValuesRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryGetMedianValuesRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryGetMedianValuesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *QueryGetMedianValuesResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryGetMedianValuesResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryGetMedianValuesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.MedianValues) > 0 { + for iNdEx := len(m.MedianValues) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MedianValues[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -1955,6 +2140,30 @@ func (m *QueryGetTimeBasedRewardsResponse) Size() (n int) { return n } +func (m *QueryGetMedianValuesRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *QueryGetMedianValuesResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.MedianValues) > 0 { + for _, e := range m.MedianValues { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } + return n +} + func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -3309,6 +3518,140 @@ func (m *QueryGetTimeBasedRewardsResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryGetMedianValuesRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryGetMedianValuesRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryGetMedianValuesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryGetMedianValuesResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryGetMedianValuesResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryGetMedianValuesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MedianValues", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MedianValues = append(m.MedianValues, &MedianValues{}) + if err := m.MedianValues[len(m.MedianValues)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/oracle/types/query.pb.gw.go b/x/oracle/types/query.pb.gw.go index f074f54b1..8d667df36 100644 --- a/x/oracle/types/query.pb.gw.go +++ b/x/oracle/types/query.pb.gw.go @@ -513,6 +513,24 @@ func local_request_Query_GetTimeBasedRewards_0(ctx context.Context, marshaler ru } +func request_Query_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryGetMedianValuesRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetMedianValues(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryGetMedianValuesRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetMedianValues(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -726,6 +744,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_GetMedianValues_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -947,6 +988,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_GetMedianValues_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -968,6 +1029,8 @@ var ( pattern_Query_GetDataBefore_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5}, []string{"tellor-io", "layer", "oracle", "GetDataBefore", "queryId", "timestamp"}, "", runtime.AssumeColonVerbOpt(true))) pattern_Query_GetTimeBasedRewards_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"tellor-io", "layer", "oracle", "get_time_based_rewards"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_Query_GetMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"tellor-io", "layer", "oracle", "get_median_values"}, "", runtime.AssumeColonVerbOpt(true))) ) var ( @@ -988,4 +1051,6 @@ var ( forward_Query_GetDataBefore_0 = runtime.ForwardResponseMessage forward_Query_GetTimeBasedRewards_0 = runtime.ForwardResponseMessage + + forward_Query_GetMedianValues_0 = runtime.ForwardResponseMessage ) From 81cdc9cf259be298d6389202f6bd326582cb692e Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 17 Jan 2024 09:26:26 -0500 Subject: [PATCH 02/10] add median server and decouple from oracle keeper --- app/app.go | 111 ++- daemons/server/median/median.go | 53 ++ daemons/server/types/median_values.pb.go | 737 ++++++++++++++++++ daemons/server/types/median_values.pb.gw.go | 153 ++++ docs/static/openapi.yml | 64 -- go.mod | 2 +- proto/layer/daemons/median_values.proto | 28 + proto/layer/oracle/median_values.proto | 10 - proto/layer/oracle/query.proto | 15 +- ...t_median_values.go => query_get_median.go} | 8 +- x/oracle/keeper/keeper.go | 42 +- x/oracle/keeper/keeper_test.go | 2 - x/oracle/keeper/query_get_median_values.go | 32 - x/oracle/module.go | 2 - x/oracle/types/query.pb.go | 475 ++--------- x/oracle/types/query.pb.gw.go | 65 -- 16 files changed, 1116 insertions(+), 683 deletions(-) create mode 100644 daemons/server/median/median.go create mode 100644 daemons/server/types/median_values.pb.go create mode 100644 daemons/server/types/median_values.pb.gw.go create mode 100644 proto/layer/daemons/median_values.proto delete mode 100644 proto/layer/oracle/median_values.proto rename x/oracle/client/cli/{query_get_median_values.go => query_get_median.go} (75%) delete mode 100644 x/oracle/keeper/query_get_median_values.go diff --git a/app/app.go b/app/app.go index e2013325f..c99676223 100644 --- a/app/app.go +++ b/app/app.go @@ -136,6 +136,7 @@ import ( daemontypes "github.com/tellor-io/layer/daemons/types" pricefeedclient "github.com/tellor-io/layer/daemons/pricefeed/client" + medianserver "github.com/tellor-io/layer/daemons/server/median" pricefeedtypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" ) @@ -295,7 +296,7 @@ type App struct { configurator module.Configurator Server *daemonserver.Server - startDaemons func() + startDaemons func(client.Context, *api.Server) PriceFeedClient *pricefeedclient.Client } @@ -564,7 +565,33 @@ func New( app.GetSubspace(registrymoduletypes.ModuleName), ) registryModule := registrymodule.NewAppModule(appCodec, app.RegistryKeeper, app.AccountKeeper, app.BankKeeper) - indexPriceCache := pricefeedtypes.NewMarketToExchangePrices(constants.MaxPriceAge) + + app.OracleKeeper = *oraclemodulekeeper.NewKeeper( + appCodec, + keys[oraclemoduletypes.StoreKey], + keys[oraclemoduletypes.MemStoreKey], + app.AccountKeeper, + app.BankKeeper, + app.DistrKeeper, + app.StakingKeeper, + app.RegistryKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + oracleModule := oraclemodule.NewAppModule(appCodec, app.OracleKeeper, app.AccountKeeper, app.BankKeeper) + + app.DisputeKeeper = *disputemodulekeeper.NewKeeper( + appCodec, + keys[disputemoduletypes.StoreKey], + keys[disputemoduletypes.MemStoreKey], + app.GetSubspace(disputemoduletypes.ModuleName), + + app.AccountKeeper, + app.BankKeeper, + app.OracleKeeper, + app.SlashingKeeper, + app.StakingKeeper, + ) + disputeModule := disputemodule.NewAppModule(appCodec, app.DisputeKeeper, app.AccountKeeper, app.BankKeeper) // this line is used by starport scaffolding # stargate/app/keeperDefinition appFlags := flags.GetFlagValuesFromOptions(appOpts) @@ -573,10 +600,11 @@ func New( if err := appFlags.Validate(); err != nil { panic(err) } - // Get Daemon Flags. daemonFlags := daemonflags.GetDaemonFlagValuesFromOptions(appOpts) logger.Info("Parsed Daemon flags", "Flags", daemonFlags) + + indexPriceCache := pricefeedtypes.NewMarketToExchangePrices(constants.MaxPriceAge) // Create server that will ingest gRPC messages from daemon clients. // Note that gRPC clients will block on new gRPC connection until the gRPC server is ready to // accept new connections. @@ -587,67 +615,38 @@ func New( daemonFlags.Shared.SocketAddress, ) app.Server.WithPriceFeedMarketToExchangePrices(indexPriceCache) - // Create a closure for starting daemons and daemon server. Daemon services are delayed until after the gRPC // service is started because daemons depend on the gRPC service being available. If a node is initialized // with a genesis time in the future, then the gRPC service will not be available until the genesis time, the // daemons will not be able to connect to the cosmos gRPC query service and finish initialization, and the daemon // monitoring service will panic. - app.startDaemons = func() { + app.startDaemons = func(cltx client.Context, apiSvr *api.Server) { // Start server for handling gRPC messages from daemons. go app.Server.Start() + // Non-validating full-nodes have no need to run the price daemon. + if !appFlags.NonValidatingFullNode && daemonFlags.Price.Enabled { + exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) + marketParamsConfig := configs.ReadMarketParamsConfigFile(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{}, + marketParamsConfig, + exchangeQueryConfig, + constants.StaticExchangeDetails, + &pricefeedclient.SubTaskRunnerImpl{}, + ) + medianserver.StartMedianServer(cltx, app.GRPCQueryRouter(), apiSvr.GRPCGatewayRouter, marketParamsConfig, indexPriceCache) + } } - // Non-validating full-nodes have no need to run the price daemon. - // if !appFlags.NonValidatingFullNode && daemonFlags.Price.Enabled { - exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) - marketParamsConfig := configs.ReadMarketParamsConfigFile(homePath) - - app.OracleKeeper = *oraclemodulekeeper.NewKeeper( - appCodec, - keys[oraclemoduletypes.StoreKey], - keys[oraclemoduletypes.MemStoreKey], - marketParamsConfig, - indexPriceCache, - app.AccountKeeper, - app.BankKeeper, - app.DistrKeeper, - app.StakingKeeper, - app.RegistryKeeper, - authtypes.NewModuleAddress(govtypes.ModuleName).String(), - ) - oracleModule := oraclemodule.NewAppModule(appCodec, app.OracleKeeper, app.AccountKeeper, app.BankKeeper) - - app.DisputeKeeper = *disputemodulekeeper.NewKeeper( - appCodec, - keys[disputemoduletypes.StoreKey], - keys[disputemoduletypes.MemStoreKey], - app.GetSubspace(disputemoduletypes.ModuleName), - - app.AccountKeeper, - app.BankKeeper, - app.OracleKeeper, - app.SlashingKeeper, - app.StakingKeeper, - ) - disputeModule := disputemodule.NewAppModule(appCodec, app.DisputeKeeper, app.AccountKeeper, app.BankKeeper) - // 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{}, - marketParamsConfig, - exchangeQueryConfig, - constants.StaticExchangeDetails, - &pricefeedclient.SubTaskRunnerImpl{}, - ) - // } /**** IBC Routing ****/ // Sealing prevents other modules from creating scoped sub-keepers @@ -995,7 +994,7 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig // register app's OpenAPI routes. docs.RegisterOpenAPIService(Name, apiSvr.Router) - app.startDaemons() + app.startDaemons(clientCtx, apiSvr) } // RegisterTxService implements the Application.RegisterTxService method. diff --git a/daemons/server/median/median.go b/daemons/server/median/median.go new file mode 100644 index 000000000..73f09adf4 --- /dev/null +++ b/daemons/server/median/median.go @@ -0,0 +1,53 @@ +package median + +import ( + "context" + "time" + + "github.com/cosmos/cosmos-sdk/client" + gogogrpc "github.com/gogo/protobuf/grpc" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/daemons/server/types" + pricefeedservertypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" +) + +var _ types.MedianValuesServiceServer = &medianServer{} + +type medianServer struct { + clientCtx client.Context + marketToExchange *pricefeedservertypes.MarketToExchangePrices + marketParams []pricefeedtypes.MarketParam +} + +func NewMedianValuesServer(clientCtx client.Context, marketToExchange *pricefeedservertypes.MarketToExchangePrices, marketParams []pricefeedtypes.MarketParam) types.MedianValuesServiceServer { + return &medianServer{ + clientCtx: clientCtx, + marketToExchange: marketToExchange, + marketParams: marketParams, + } +} + +func (s *medianServer) GetMedianValues(ctx context.Context, req *types.GetMedianValuesRequest) (*types.GetMedianValuesResponse, error) { + values := s.marketToExchange.GetValidMedianPrices(s.marketParams, time.Now()) + medianValues := make([]*types.MedianValues, 0, len(values)) + for i, value := range values { + medianValues = append(medianValues, &types.MedianValues{ + MarketId: i, + Price: value, + }) + } + return &types.GetMedianValuesResponse{MedianValues: medianValues}, nil + +} + +func StartMedianServer( + clientCtx client.Context, + server gogogrpc.Server, + mux *runtime.ServeMux, + marketParams []pricefeedtypes.MarketParam, + marketToExchange *pricefeedservertypes.MarketToExchangePrices, +) { + types.RegisterMedianValuesServiceServer(server, NewMedianValuesServer(clientCtx, marketToExchange, marketParams)) + types.RegisterMedianValuesServiceHandlerClient(context.Background(), mux, types.NewMedianValuesServiceClient(clientCtx)) +} diff --git a/daemons/server/types/median_values.pb.go b/daemons/server/types/median_values.pb.go new file mode 100644 index 000000000..37e1fd464 --- /dev/null +++ b/daemons/server/types/median_values.pb.go @@ -0,0 +1,737 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: layer/daemons/median_values.proto + +package types + +import ( + context "context" + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// MedianValues is the median value for a market +type MedianValues struct { + // marketId is the market id for a pair + MarketId uint32 `protobuf:"varint,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + Price uint64 `protobuf:"varint,2,opt,name=price,proto3" json:"price,omitempty"` +} + +func (m *MedianValues) Reset() { *m = MedianValues{} } +func (m *MedianValues) String() string { return proto.CompactTextString(m) } +func (*MedianValues) ProtoMessage() {} +func (*MedianValues) Descriptor() ([]byte, []int) { + return fileDescriptor_9b64c378347d9f4f, []int{0} +} +func (m *MedianValues) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MedianValues) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MedianValues.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MedianValues) XXX_Merge(src proto.Message) { + xxx_messageInfo_MedianValues.Merge(m, src) +} +func (m *MedianValues) XXX_Size() int { + return m.Size() +} +func (m *MedianValues) XXX_DiscardUnknown() { + xxx_messageInfo_MedianValues.DiscardUnknown(m) +} + +var xxx_messageInfo_MedianValues proto.InternalMessageInfo + +func (m *MedianValues) GetMarketId() uint32 { + if m != nil { + return m.MarketId + } + return 0 +} + +func (m *MedianValues) GetPrice() uint64 { + if m != nil { + return m.Price + } + return 0 +} + +// GetMedianValuesRequest is the request for the GetMedianValues rpc +type GetMedianValuesRequest struct { +} + +func (m *GetMedianValuesRequest) Reset() { *m = GetMedianValuesRequest{} } +func (m *GetMedianValuesRequest) String() string { return proto.CompactTextString(m) } +func (*GetMedianValuesRequest) ProtoMessage() {} +func (*GetMedianValuesRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_9b64c378347d9f4f, []int{1} +} +func (m *GetMedianValuesRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GetMedianValuesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GetMedianValuesRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GetMedianValuesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetMedianValuesRequest.Merge(m, src) +} +func (m *GetMedianValuesRequest) XXX_Size() int { + return m.Size() +} +func (m *GetMedianValuesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetMedianValuesRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetMedianValuesRequest proto.InternalMessageInfo + +// GetMedianValuesResponse is the response for the GetMedianValues rpc +type GetMedianValuesResponse struct { + MedianValues []*MedianValues `protobuf:"bytes,1,rep,name=median_values,json=medianValues,proto3" json:"median_values,omitempty"` +} + +func (m *GetMedianValuesResponse) Reset() { *m = GetMedianValuesResponse{} } +func (m *GetMedianValuesResponse) String() string { return proto.CompactTextString(m) } +func (*GetMedianValuesResponse) ProtoMessage() {} +func (*GetMedianValuesResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_9b64c378347d9f4f, []int{2} +} +func (m *GetMedianValuesResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GetMedianValuesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GetMedianValuesResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GetMedianValuesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetMedianValuesResponse.Merge(m, src) +} +func (m *GetMedianValuesResponse) XXX_Size() int { + return m.Size() +} +func (m *GetMedianValuesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetMedianValuesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetMedianValuesResponse proto.InternalMessageInfo + +func (m *GetMedianValuesResponse) GetMedianValues() []*MedianValues { + if m != nil { + return m.MedianValues + } + return nil +} + +func init() { + proto.RegisterType((*MedianValues)(nil), "layer.daemons.MedianValues") + proto.RegisterType((*GetMedianValuesRequest)(nil), "layer.daemons.GetMedianValuesRequest") + proto.RegisterType((*GetMedianValuesResponse)(nil), "layer.daemons.GetMedianValuesResponse") +} + +func init() { proto.RegisterFile("layer/daemons/median_values.proto", fileDescriptor_9b64c378347d9f4f) } + +var fileDescriptor_9b64c378347d9f4f = []byte{ + // 335 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0x31, 0x4b, 0xfb, 0x40, + 0x18, 0xc6, 0x7b, 0xfd, 0xff, 0x15, 0x3d, 0x5b, 0x84, 0xb3, 0x68, 0x68, 0xcb, 0x51, 0x83, 0x4a, + 0x17, 0x73, 0x50, 0xbf, 0x80, 0xba, 0x48, 0x07, 0x97, 0x0a, 0x0e, 0x3a, 0x94, 0x6b, 0xf3, 0x12, + 0x0f, 0x93, 0xbc, 0xf1, 0xee, 0x5a, 0xe8, 0xea, 0xe0, 0x2c, 0xb8, 0xfb, 0x79, 0x1c, 0x0b, 0x2e, + 0x8e, 0xd2, 0xfa, 0x41, 0xa4, 0x49, 0x87, 0xa4, 0x0a, 0x6e, 0x79, 0xf3, 0x3c, 0xf7, 0xbb, 0xf7, + 0xb9, 0x87, 0xee, 0x87, 0x72, 0x02, 0x5a, 0xf8, 0x12, 0x22, 0x8c, 0x8d, 0x88, 0xc0, 0x57, 0x32, + 0xee, 0x8f, 0x65, 0x38, 0x02, 0xe3, 0x25, 0x1a, 0x2d, 0xb2, 0x6a, 0x6a, 0xf1, 0x96, 0x96, 0x7a, + 0x2d, 0xc0, 0x00, 0x53, 0x45, 0x2c, 0xbe, 0x32, 0x53, 0xbd, 0x19, 0x20, 0x06, 0x21, 0x08, 0x99, + 0x28, 0x21, 0xe3, 0x18, 0xad, 0xb4, 0x0a, 0xe3, 0x25, 0xc2, 0x3d, 0xa3, 0x95, 0xcb, 0x94, 0x7c, + 0x9d, 0x82, 0x59, 0x83, 0x6e, 0x46, 0x52, 0xdf, 0x83, 0xed, 0x2b, 0xdf, 0x21, 0x2d, 0xd2, 0xae, + 0xf6, 0x36, 0xb2, 0x1f, 0x5d, 0x9f, 0xd5, 0xe8, 0x5a, 0xa2, 0xd5, 0x10, 0x9c, 0x72, 0x8b, 0xb4, + 0xff, 0xf7, 0xb2, 0xc1, 0x75, 0xe8, 0xee, 0x05, 0xd8, 0x3c, 0xa5, 0x07, 0x0f, 0x23, 0x30, 0xd6, + 0xbd, 0xa5, 0x7b, 0x3f, 0x14, 0x93, 0x60, 0x6c, 0x80, 0x9d, 0xd2, 0x6a, 0x21, 0x91, 0x43, 0x5a, + 0xff, 0xda, 0x5b, 0x9d, 0x86, 0x57, 0x88, 0xe4, 0x15, 0xce, 0x56, 0xa2, 0xdc, 0xd4, 0x79, 0x25, + 0x74, 0x27, 0x2f, 0x5f, 0x81, 0x1e, 0xab, 0x21, 0xb0, 0x27, 0x42, 0xb7, 0x57, 0x6e, 0x65, 0x87, + 0x2b, 0xd8, 0xdf, 0xf7, 0xad, 0x1f, 0xfd, 0x65, 0xcb, 0x96, 0x77, 0x0f, 0x1e, 0xdf, 0xbf, 0x5e, + 0xca, 0x9c, 0x35, 0x45, 0xd6, 0xd1, 0xe2, 0x69, 0x03, 0xb0, 0xfd, 0x42, 0xa2, 0xf3, 0xee, 0xdb, + 0x8c, 0x93, 0xe9, 0x8c, 0x93, 0xcf, 0x19, 0x27, 0xcf, 0x73, 0x5e, 0x9a, 0xce, 0x79, 0xe9, 0x63, + 0xce, 0x4b, 0x37, 0x22, 0x50, 0xf6, 0x6e, 0x34, 0xf0, 0x86, 0x18, 0x09, 0x0b, 0x61, 0x88, 0xfa, + 0x58, 0xa1, 0x28, 0xf6, 0x6d, 0x40, 0x8f, 0x41, 0x0b, 0x3b, 0x49, 0xc0, 0x0c, 0xd6, 0xd3, 0xb2, + 0x4e, 0xbe, 0x03, 0x00, 0x00, 0xff, 0xff, 0x00, 0x52, 0x3d, 0xbd, 0x14, 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// MedianValuesServiceClient is the client API for MedianValuesService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type MedianValuesServiceClient interface { + // GetMedianValues returns the median values for all markets + GetMedianValues(ctx context.Context, in *GetMedianValuesRequest, opts ...grpc.CallOption) (*GetMedianValuesResponse, error) +} + +type medianValuesServiceClient struct { + cc grpc1.ClientConn +} + +func NewMedianValuesServiceClient(cc grpc1.ClientConn) MedianValuesServiceClient { + return &medianValuesServiceClient{cc} +} + +func (c *medianValuesServiceClient) GetMedianValues(ctx context.Context, in *GetMedianValuesRequest, opts ...grpc.CallOption) (*GetMedianValuesResponse, error) { + out := new(GetMedianValuesResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.MedianValuesService/GetMedianValues", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MedianValuesServiceServer is the server API for MedianValuesService service. +type MedianValuesServiceServer interface { + // GetMedianValues returns the median values for all markets + GetMedianValues(context.Context, *GetMedianValuesRequest) (*GetMedianValuesResponse, error) +} + +// UnimplementedMedianValuesServiceServer can be embedded to have forward compatible implementations. +type UnimplementedMedianValuesServiceServer struct { +} + +func (*UnimplementedMedianValuesServiceServer) GetMedianValues(ctx context.Context, req *GetMedianValuesRequest) (*GetMedianValuesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMedianValues not implemented") +} + +func RegisterMedianValuesServiceServer(s grpc1.Server, srv MedianValuesServiceServer) { + s.RegisterService(&_MedianValuesService_serviceDesc, srv) +} + +func _MedianValuesService_GetMedianValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMedianValuesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MedianValuesServiceServer).GetMedianValues(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.daemons.MedianValuesService/GetMedianValues", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MedianValuesServiceServer).GetMedianValues(ctx, req.(*GetMedianValuesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _MedianValuesService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "layer.daemons.MedianValuesService", + HandlerType: (*MedianValuesServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetMedianValues", + Handler: _MedianValuesService_GetMedianValues_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "layer/daemons/median_values.proto", +} + +func (m *MedianValues) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MedianValues) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MedianValues) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Price != 0 { + i = encodeVarintMedianValues(dAtA, i, uint64(m.Price)) + i-- + dAtA[i] = 0x10 + } + if m.MarketId != 0 { + i = encodeVarintMedianValues(dAtA, i, uint64(m.MarketId)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *GetMedianValuesRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetMedianValuesRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GetMedianValuesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *GetMedianValuesResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetMedianValuesResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GetMedianValuesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.MedianValues) > 0 { + for iNdEx := len(m.MedianValues) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MedianValues[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMedianValues(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintMedianValues(dAtA []byte, offset int, v uint64) int { + offset -= sovMedianValues(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MedianValues) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.MarketId != 0 { + n += 1 + sovMedianValues(uint64(m.MarketId)) + } + if m.Price != 0 { + n += 1 + sovMedianValues(uint64(m.Price)) + } + return n +} + +func (m *GetMedianValuesRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *GetMedianValuesResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.MedianValues) > 0 { + for _, e := range m.MedianValues { + l = e.Size() + n += 1 + l + sovMedianValues(uint64(l)) + } + } + return n +} + +func sovMedianValues(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozMedianValues(x uint64) (n int) { + return sovMedianValues(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MedianValues) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MedianValues: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MedianValues: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MarketId", wireType) + } + m.MarketId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MarketId |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) + } + m.Price = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Price |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMedianValues(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMedianValues + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetMedianValuesRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetMedianValuesRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetMedianValuesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipMedianValues(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMedianValues + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetMedianValuesResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetMedianValuesResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetMedianValuesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MedianValues", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMedianValues + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMedianValues + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MedianValues = append(m.MedianValues, &MedianValues{}) + if err := m.MedianValues[len(m.MedianValues)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMedianValues(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMedianValues + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipMedianValues(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMedianValues + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMedianValues + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMedianValues + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthMedianValues + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupMedianValues + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthMedianValues + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthMedianValues = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowMedianValues = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupMedianValues = fmt.Errorf("proto: unexpected end of group") +) diff --git a/daemons/server/types/median_values.pb.gw.go b/daemons/server/types/median_values.pb.gw.go new file mode 100644 index 000000000..6c1a03304 --- /dev/null +++ b/daemons/server/types/median_values.pb.gw.go @@ -0,0 +1,153 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: layer/daemons/median_values.proto + +/* +Package types is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package types + +import ( + "context" + "io" + "net/http" + + "github.com/golang/protobuf/descriptor" + "github.com/golang/protobuf/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = descriptor.ForMessage +var _ = metadata.Join + +func request_MedianValuesService_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, client MedianValuesServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetMedianValuesRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetMedianValues(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_MedianValuesService_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, server MedianValuesServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetMedianValuesRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetMedianValues(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterMedianValuesServiceHandlerServer registers the http handlers for service MedianValuesService to "mux". +// UnaryRPC :call MedianValuesServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterMedianValuesServiceHandlerFromEndpoint instead. +func RegisterMedianValuesServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server MedianValuesServiceServer) error { + + mux.Handle("GET", pattern_MedianValuesService_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MedianValuesService_GetMedianValues_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_MedianValuesService_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterMedianValuesServiceHandlerFromEndpoint is same as RegisterMedianValuesServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterMedianValuesServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterMedianValuesServiceHandler(ctx, mux, conn) +} + +// RegisterMedianValuesServiceHandler registers the http handlers for service MedianValuesService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterMedianValuesServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterMedianValuesServiceHandlerClient(ctx, mux, NewMedianValuesServiceClient(conn)) +} + +// RegisterMedianValuesServiceHandlerClient registers the http handlers for service MedianValuesService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "MedianValuesServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "MedianValuesServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "MedianValuesServiceClient" to call the correct interceptors. +func RegisterMedianValuesServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client MedianValuesServiceClient) error { + + mux.Handle("GET", pattern_MedianValuesService_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MedianValuesService_GetMedianValues_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_MedianValuesService_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_MedianValuesService_GetMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"layer", "api", "get_median_values"}, "", runtime.AssumeColonVerbOpt(true))) +) + +var ( + forward_MedianValuesService_GetMedianValues_0 = runtime.ForwardResponseMessage +) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 80b346c1e..183eeb8b5 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46910,47 +46910,6 @@ paths: type: string tags: - Query - /tellor-io/layer/oracle/get_median_values: - get: - summary: Queries a list of GetMedianValues items. - operationId: LayerOracleGetMedianValues - responses: - '200': - description: A successful response. - schema: - type: object - properties: - medianValues: - type: array - items: - type: object - properties: - marketId: - type: integer - format: int64 - price: - type: string - format: uint64 - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query /tellor-io/layer/oracle/get_time_based_rewards: get: summary: Queries a list of GetTimeBasedRewards items. @@ -76003,15 +75962,6 @@ definitions: power: type: string format: int64 - layer.oracle.MedianValues: - type: object - properties: - marketId: - type: integer - format: int64 - price: - type: string - format: uint64 layer.oracle.MicroReport: type: object properties: @@ -76124,20 +76074,6 @@ definitions: method signatures required by gogoproto. - layer.oracle.QueryGetMedianValuesResponse: - type: object - properties: - medianValues: - type: array - items: - type: object - properties: - marketId: - type: integer - format: int64 - price: - type: string - format: uint64 layer.oracle.QueryGetReportsbyQidResponse: type: object properties: diff --git a/go.mod b/go.mod index 25acc54b4..985567fd9 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/cosmos/ibc-go/v7 v7.1.0 github.com/ethereum/go-ethereum v1.10.22 github.com/go-playground/validator/v10 v10.12.0 + github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.3 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 @@ -91,7 +92,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/mock v1.6.0 // indirect diff --git a/proto/layer/daemons/median_values.proto b/proto/layer/daemons/median_values.proto new file mode 100644 index 000000000..639bf9291 --- /dev/null +++ b/proto/layer/daemons/median_values.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package layer.daemons; + +import "gogoproto/gogo.proto"; +import "google/api/annotations.proto"; + +option go_package = "github.com/tellor-io/layer/daemons/server/types"; + +// MedianValuesService is the service that provides median values for markets +service MedianValuesService { + // GetMedianValues returns the median values for all markets + rpc GetMedianValues(GetMedianValuesRequest) returns (GetMedianValuesResponse) { + option (google.api.http).get = "/layer/api/get_median_values"; + } +} +// MedianValues is the median value for a market +message MedianValues { + // marketId is the market id for a pair + uint32 market_id = 1; + uint64 price = 2; +} +// GetMedianValuesRequest is the request for the GetMedianValues rpc +message GetMedianValuesRequest {} +// GetMedianValuesResponse is the response for the GetMedianValues rpc +message GetMedianValuesResponse { + repeated MedianValues median_values = 1; +} diff --git a/proto/layer/oracle/median_values.proto b/proto/layer/oracle/median_values.proto deleted file mode 100644 index a45cdcec0..000000000 --- a/proto/layer/oracle/median_values.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto3"; -package layer.oracle; - -option go_package = "github.com/tellor-io/layer/x/oracle/types"; - -message MedianValues { - - uint32 marketId = 1; - uint64 price = 2; -} diff --git a/proto/layer/oracle/query.proto b/proto/layer/oracle/query.proto index 7bd7d5737..1a1f1d485 100644 --- a/proto/layer/oracle/query.proto +++ b/proto/layer/oracle/query.proto @@ -13,7 +13,6 @@ import "layer/oracle/user_tip.proto"; import "layer/oracle/aggregate.proto"; import "amino/amino.proto"; import "cosmos/base/v1beta1/coin.proto"; -import "layer/oracle/median_values.proto"; option go_package = "github.com/tellor-io/layer/x/oracle/types"; @@ -69,12 +68,7 @@ service Query { option (google.api.http).get = "/tellor-io/layer/oracle/get_time_based_rewards"; } - - // Queries a list of GetMedianValues items. - rpc GetMedianValues (QueryGetMedianValuesRequest) returns (QueryGetMedianValuesResponse) { - option (google.api.http).get = "/tellor-io/layer/oracle/get_median_values"; - - } + } // QueryParamsRequest is request type for the Query/Params RPC method. message QueryParamsRequest {} @@ -142,10 +136,3 @@ message QueryGetTimeBasedRewardsRequest {} message QueryGetTimeBasedRewardsResponse { cosmos.base.v1beta1.Coin reward = 1 [(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coin", (gogoproto.nullable) = false, (amino.dont_omitempty) = true]; } - -message QueryGetMedianValuesRequest {} - -message QueryGetMedianValuesResponse { - repeated MedianValues medianValues = 1; -} - diff --git a/x/oracle/client/cli/query_get_median_values.go b/x/oracle/client/cli/query_get_median.go similarity index 75% rename from x/oracle/client/cli/query_get_median_values.go rename to x/oracle/client/cli/query_get_median.go index 81d33cf87..1d8bb7e7b 100644 --- a/x/oracle/client/cli/query_get_median_values.go +++ b/x/oracle/client/cli/query_get_median.go @@ -1,12 +1,13 @@ package cli import ( + "fmt" "strconv" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "github.com/tellor-io/layer/x/oracle/types" + mediantypes "github.com/tellor-io/layer/daemons/server/types" ) var _ = strconv.Itoa(0) @@ -22,10 +23,11 @@ func CmdGetMedianValues() *cobra.Command { if err != nil { return err } + fmt.Println(clientCtx.Height) - queryClient := types.NewQueryClient(clientCtx) + queryClient := mediantypes.NewMedianValuesServiceClient(clientCtx) - params := &types.QueryGetMedianValuesRequest{} + params := &mediantypes.GetMedianValuesRequest{} res, err := queryClient.GetMedianValues(cmd.Context(), params) if err != nil { diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 06500ae4b..7933ef21c 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -9,23 +9,19 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" - daemontypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" - pricefeedtypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" "github.com/tellor-io/layer/x/oracle/types" ) type ( Keeper struct { - cdc codec.BinaryCodec - storeKey storetypes.StoreKey - memKey storetypes.StoreKey - marketParamConfig []daemontypes.MarketParam - indexPriceCache *pricefeedtypes.MarketToExchangePrices - accountKeeper types.AccountKeeper - bankKeeper types.BankKeeper - distrKeeper types.DistrKeeper - stakingKeeper types.StakingKeeper - registryKeeper types.RegistryKeeper + cdc codec.BinaryCodec + storeKey storetypes.StoreKey + memKey storetypes.StoreKey + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + distrKeeper types.DistrKeeper + stakingKeeper types.StakingKeeper + registryKeeper types.RegistryKeeper // the address capable of executing a MsgUpdateParams message. Typically, this // should be the x/gov module account. authority string @@ -36,8 +32,6 @@ func NewKeeper( cdc codec.BinaryCodec, storeKey, memKey storetypes.StoreKey, - marketParamConfig []daemontypes.MarketParam, - indexPriceCache *pricefeedtypes.MarketToExchangePrices, accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, distrKeeper types.DistrKeeper, @@ -50,17 +44,15 @@ func NewKeeper( } return &Keeper{ - cdc: cdc, - storeKey: storeKey, - memKey: memKey, - marketParamConfig: marketParamConfig, - indexPriceCache: indexPriceCache, - accountKeeper: accountKeeper, - bankKeeper: bankKeeper, - distrKeeper: distrKeeper, - stakingKeeper: stakingKeeper, - registryKeeper: registryKeeper, - authority: authority, + cdc: cdc, + storeKey: storeKey, + memKey: memKey, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + distrKeeper: distrKeeper, + stakingKeeper: stakingKeeper, + registryKeeper: registryKeeper, + authority: authority, } } diff --git a/x/oracle/keeper/keeper_test.go b/x/oracle/keeper/keeper_test.go index 36524c7e5..690253ad3 100644 --- a/x/oracle/keeper/keeper_test.go +++ b/x/oracle/keeper/keeper_test.go @@ -89,8 +89,6 @@ func (s *KeeperTestSuite) SetupTest() { cdc, storeKey, memStoreKey, - nil, - nil, s.accountKeeper, nil, s.distrKeeper, diff --git a/x/oracle/keeper/query_get_median_values.go b/x/oracle/keeper/query_get_median_values.go deleted file mode 100644 index d866cd488..000000000 --- a/x/oracle/keeper/query_get_median_values.go +++ /dev/null @@ -1,32 +0,0 @@ -package keeper - -import ( - "context" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/tellor-io/layer/x/oracle/types" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func (k Keeper) GetMedianValues(goCtx context.Context, req *types.QueryGetMedianValuesRequest) (*types.QueryGetMedianValuesResponse, error) { - if req == nil { - return nil, status.Error(codes.InvalidArgument, "invalid request") - } - - ctx := sdk.UnwrapSDKContext(goCtx) - - // TODO: Process the query - _ = ctx - - values := k.indexPriceCache.GetValidMedianPrices(k.marketParamConfig, time.Now()) - medianValues := make([]*types.MedianValues, 0, len(values)) - for i, value := range values { - medianValues = append(medianValues, &types.MedianValues{ - MarketId: i, - Price: value, - }) - } - return &types.QueryGetMedianValuesResponse{MedianValues: medianValues}, nil -} diff --git a/x/oracle/module.go b/x/oracle/module.go index 2bc8224d2..b52f80167 100644 --- a/x/oracle/module.go +++ b/x/oracle/module.go @@ -201,8 +201,6 @@ func ProvideModule(in OracleInputs) OracleOutputs { in.Cdc, in.KvStoreKey, in.MemStoreKey, - nil, - nil, in.AccountKeeper, in.BankKeeper, in.Distr, diff --git a/x/oracle/types/query.pb.go b/x/oracle/types/query.pb.go index e6b19a52c..e1fd3cff9 100644 --- a/x/oracle/types/query.pb.go +++ b/x/oracle/types/query.pb.go @@ -749,86 +749,6 @@ func (m *QueryGetTimeBasedRewardsResponse) GetReward() types.Coin { return types.Coin{} } -type QueryGetMedianValuesRequest struct { -} - -func (m *QueryGetMedianValuesRequest) Reset() { *m = QueryGetMedianValuesRequest{} } -func (m *QueryGetMedianValuesRequest) String() string { return proto.CompactTextString(m) } -func (*QueryGetMedianValuesRequest) ProtoMessage() {} -func (*QueryGetMedianValuesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_bb27caa9d264c5c4, []int{16} -} -func (m *QueryGetMedianValuesRequest) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *QueryGetMedianValuesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_QueryGetMedianValuesRequest.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *QueryGetMedianValuesRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_QueryGetMedianValuesRequest.Merge(m, src) -} -func (m *QueryGetMedianValuesRequest) XXX_Size() int { - return m.Size() -} -func (m *QueryGetMedianValuesRequest) XXX_DiscardUnknown() { - xxx_messageInfo_QueryGetMedianValuesRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_QueryGetMedianValuesRequest proto.InternalMessageInfo - -type QueryGetMedianValuesResponse struct { - MedianValues []*MedianValues `protobuf:"bytes,1,rep,name=medianValues,proto3" json:"medianValues,omitempty"` -} - -func (m *QueryGetMedianValuesResponse) Reset() { *m = QueryGetMedianValuesResponse{} } -func (m *QueryGetMedianValuesResponse) String() string { return proto.CompactTextString(m) } -func (*QueryGetMedianValuesResponse) ProtoMessage() {} -func (*QueryGetMedianValuesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_bb27caa9d264c5c4, []int{17} -} -func (m *QueryGetMedianValuesResponse) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *QueryGetMedianValuesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_QueryGetMedianValuesResponse.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *QueryGetMedianValuesResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_QueryGetMedianValuesResponse.Merge(m, src) -} -func (m *QueryGetMedianValuesResponse) XXX_Size() int { - return m.Size() -} -func (m *QueryGetMedianValuesResponse) XXX_DiscardUnknown() { - xxx_messageInfo_QueryGetMedianValuesResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_QueryGetMedianValuesResponse proto.InternalMessageInfo - -func (m *QueryGetMedianValuesResponse) GetMedianValues() []*MedianValues { - if m != nil { - return m.MedianValues - } - return nil -} - func init() { proto.RegisterType((*QueryParamsRequest)(nil), "layer.oracle.QueryParamsRequest") proto.RegisterType((*QueryParamsResponse)(nil), "layer.oracle.QueryParamsResponse") @@ -846,84 +766,77 @@ func init() { proto.RegisterType((*QueryGetDataBeforeRequest)(nil), "layer.oracle.QueryGetDataBeforeRequest") proto.RegisterType((*QueryGetTimeBasedRewardsRequest)(nil), "layer.oracle.QueryGetTimeBasedRewardsRequest") proto.RegisterType((*QueryGetTimeBasedRewardsResponse)(nil), "layer.oracle.QueryGetTimeBasedRewardsResponse") - proto.RegisterType((*QueryGetMedianValuesRequest)(nil), "layer.oracle.QueryGetMedianValuesRequest") - proto.RegisterType((*QueryGetMedianValuesResponse)(nil), "layer.oracle.QueryGetMedianValuesResponse") } func init() { proto.RegisterFile("layer/oracle/query.proto", fileDescriptor_bb27caa9d264c5c4) } var fileDescriptor_bb27caa9d264c5c4 = []byte{ - // 1108 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x97, 0x41, 0x6f, 0x1b, 0x45, - 0x14, 0xc7, 0xb3, 0x69, 0x71, 0xc9, 0x34, 0x15, 0x62, 0x6a, 0x9a, 0x64, 0x1b, 0x9c, 0x64, 0x91, - 0xda, 0x24, 0x28, 0x3b, 0x38, 0x6d, 0x09, 0x55, 0xd5, 0x46, 0xb5, 0x8b, 0xaa, 0x1e, 0x2a, 0x51, - 0xc7, 0x45, 0x08, 0x24, 0xac, 0xb1, 0x3d, 0x6c, 0x47, 0xb5, 0x3d, 0x9b, 0xdd, 0x71, 0xc1, 0x8a, - 0x7c, 0xe1, 0xca, 0x05, 0x89, 0x13, 0xe2, 0xc0, 0x15, 0x21, 0x90, 0xca, 0x1d, 0x89, 0x6b, 0x8f, - 0x91, 0xb8, 0x70, 0x02, 0x94, 0x20, 0xf1, 0x35, 0xd0, 0xce, 0xbc, 0xf5, 0xee, 0xd8, 0xbb, 0x4e, - 0xe8, 0x25, 0xd9, 0x9d, 0x79, 0xef, 0xed, 0xef, 0xbd, 0x79, 0xfb, 0x7f, 0x6b, 0xb4, 0xd8, 0xa1, - 0x03, 0x16, 0x10, 0x11, 0xd0, 0x56, 0x87, 0x91, 0xfd, 0x3e, 0x0b, 0x06, 0xae, 0x1f, 0x08, 0x29, - 0xf0, 0xbc, 0xda, 0x71, 0xf5, 0x8e, 0x5d, 0xf4, 0x84, 0x27, 0xd4, 0x06, 0x89, 0xae, 0xb4, 0x8d, - 0xbd, 0xec, 0x09, 0xe1, 0x75, 0x18, 0xa1, 0x3e, 0x27, 0xb4, 0xd7, 0x13, 0x92, 0x4a, 0x2e, 0x7a, - 0x21, 0xec, 0x6e, 0xb6, 0x44, 0xd8, 0x15, 0x21, 0x69, 0xd2, 0x10, 0x42, 0x93, 0x67, 0xe5, 0x26, - 0x93, 0xb4, 0x4c, 0x7c, 0xea, 0xf1, 0x9e, 0x32, 0x06, 0xdb, 0x25, 0x83, 0xc3, 0xa7, 0x01, 0xed, - 0xc6, 0x61, 0x6c, 0x63, 0x2b, 0x60, 0xbe, 0x08, 0x64, 0xbc, 0xb7, 0x62, 0xec, 0x75, 0x79, 0x2b, - 0x10, 0x0d, 0x6d, 0x01, 0x06, 0x0b, 0x86, 0x81, 0xe4, 0x7e, 0xec, 0x79, 0xd9, 0xd8, 0xe8, 0x87, - 0x2c, 0x68, 0x48, 0xee, 0xc7, 0x79, 0x19, 0x9b, 0xd4, 0xf3, 0x02, 0xe6, 0x51, 0xc9, 0x60, 0xf7, - 0x75, 0xda, 0xe5, 0x3d, 0x41, 0xd4, 0x5f, 0x58, 0x2a, 0xa5, 0x53, 0x8d, 0x93, 0x6c, 0x09, 0x1e, - 0xa7, 0xb7, 0x6a, 0x72, 0xb2, 0x36, 0xa7, 0xbd, 0xc6, 0x33, 0xda, 0xe9, 0x33, 0xe0, 0x71, 0x8a, - 0x08, 0x3f, 0x8a, 0x4a, 0xf4, 0x81, 0x4a, 0xbd, 0xc6, 0xf6, 0xfb, 0x2c, 0x94, 0xce, 0x03, 0x74, - 0xd1, 0x58, 0x0d, 0x7d, 0xd1, 0x0b, 0x19, 0xde, 0x46, 0x05, 0x5d, 0xa2, 0x45, 0x6b, 0xd5, 0x5a, - 0x3f, 0xbf, 0x5d, 0x74, 0xd3, 0x87, 0xe5, 0x6a, 0xeb, 0xca, 0xd9, 0x17, 0x7f, 0xae, 0xcc, 0xd4, - 0xc0, 0xd2, 0xd9, 0x41, 0x97, 0x55, 0xa8, 0xfb, 0x4c, 0xd6, 0x74, 0x0d, 0x9b, 0x83, 0x47, 0xbc, - 0x0d, 0x4f, 0xc2, 0x8b, 0xe8, 0x9c, 0x3a, 0xa2, 0x07, 0x6d, 0x15, 0x73, 0xae, 0x16, 0xdf, 0x3a, - 0x8f, 0xd1, 0x72, 0xb6, 0x23, 0xc0, 0xdc, 0x40, 0xe7, 0xe0, 0x50, 0x80, 0xe6, 0x0d, 0x93, 0x06, - 0x9c, 0x00, 0x27, 0xb6, 0x75, 0xee, 0xa0, 0xd5, 0x89, 0xb0, 0xfa, 0x82, 0x05, 0x31, 0x94, 0x8d, - 0x5e, 0x0d, 0x60, 0x09, 0xa8, 0x46, 0xf7, 0xce, 0x13, 0xb4, 0x36, 0xc5, 0x1f, 0xd8, 0xaa, 0x68, - 0x5e, 0x35, 0x45, 0x6d, 0x04, 0x78, 0x66, 0xfd, 0xfc, 0xf6, 0x92, 0x09, 0xf8, 0x30, 0xb1, 0x00, - 0x48, 0xc3, 0xc9, 0xf9, 0x04, 0xbd, 0x95, 0xfb, 0xa4, 0x54, 0x05, 0xa7, 0xc0, 0xa6, 0xab, 0x3b, - 0x6b, 0x56, 0xf7, 0x26, 0x5a, 0x8a, 0x83, 0x57, 0xfb, 0x41, 0xc0, 0x7a, 0xb2, 0xce, 0xfd, 0x38, - 0xe4, 0x32, 0x9a, 0x53, 0x76, 0xf7, 0xa8, 0xa4, 0x10, 0x33, 0x59, 0x70, 0xee, 0x21, 0x3b, 0xcb, - 0x15, 0x52, 0xbf, 0x82, 0xce, 0x46, 0xed, 0x0e, 0x67, 0x82, 0xcd, 0x94, 0xeb, 0xdc, 0x0f, 0x6b, - 0x6a, 0xdf, 0xd9, 0x4b, 0xfa, 0xe2, 0x71, 0xc8, 0x82, 0x3a, 0xf7, 0xeb, 0x42, 0xd2, 0x4e, 0x8c, - 0x70, 0x09, 0x15, 0x24, 0xf7, 0xfd, 0x51, 0x4e, 0x70, 0x67, 0xa2, 0xcd, 0x8e, 0xa3, 0x7d, 0x94, - 0xf4, 0x8c, 0x19, 0x14, 0xe0, 0xde, 0x43, 0x73, 0x32, 0x5a, 0xa8, 0x27, 0x84, 0xb6, 0x49, 0x68, - 0xb8, 0x25, 0xc6, 0x4e, 0x05, 0x5d, 0x19, 0x4b, 0xfa, 0x6e, 0xfc, 0x7a, 0xb6, 0xf5, 0xa1, 0x9c, - 0xdc, 0xd1, 0x7b, 0x49, 0xeb, 0x4d, 0x3a, 0x03, 0x21, 0x41, 0x05, 0x7d, 0x7a, 0x80, 0xb7, 0x60, - 0xe2, 0x8d, 0xfc, 0x6a, 0x60, 0xe6, 0xec, 0x25, 0x07, 0x19, 0x95, 0xa0, 0xc2, 0x3e, 0x13, 0x01, - 0x3b, 0x91, 0x25, 0xaa, 0xa3, 0xe4, 0x5d, 0x16, 0x4a, 0xda, 0xf5, 0x55, 0x1d, 0xcf, 0xd4, 0x92, - 0x05, 0x67, 0x0d, 0xad, 0xc4, 0x41, 0xeb, 0xbc, 0xcb, 0x2a, 0x34, 0x8c, 0x40, 0x3f, 0xa7, 0x41, - 0x7b, 0x24, 0x11, 0x5f, 0x59, 0x49, 0x36, 0x93, 0x36, 0x90, 0x8d, 0x17, 0x65, 0x13, 0x2d, 0x41, - 0x36, 0x4b, 0xae, 0x16, 0x2c, 0x37, 0x12, 0x2c, 0x17, 0x04, 0xcb, 0xad, 0x0a, 0xde, 0xab, 0x5c, - 0x8f, 0xde, 0x80, 0x1f, 0xff, 0x5a, 0xb9, 0xea, 0x71, 0xf9, 0xa4, 0xdf, 0x74, 0x5b, 0xa2, 0x4b, - 0x40, 0xdd, 0xf4, 0xbf, 0xad, 0xb0, 0xfd, 0x94, 0xc8, 0x81, 0xcf, 0x42, 0xe5, 0xf0, 0xc3, 0xbf, - 0xcf, 0x37, 0xad, 0x1a, 0x84, 0x77, 0xde, 0x4c, 0xba, 0xe9, 0xa1, 0x52, 0xb9, 0x0f, 0x95, 0xc8, - 0xc5, 0xb0, 0x9f, 0x26, 0x7d, 0x61, 0x6e, 0x03, 0xe7, 0x1d, 0x34, 0xdf, 0x4d, 0xad, 0xc3, 0xfb, - 0x3a, 0xd6, 0x1a, 0x86, 0xa7, 0x61, 0xbf, 0xfd, 0xed, 0x05, 0xf4, 0x8a, 0x7a, 0x00, 0x7e, 0x8a, - 0x0a, 0x5a, 0x06, 0xf1, 0xaa, 0xe9, 0x3d, 0xa9, 0xb2, 0xf6, 0xda, 0x14, 0x0b, 0x0d, 0xe6, 0x2c, - 0x7f, 0xf9, 0xfb, 0x3f, 0xdf, 0xcc, 0x5e, 0xc2, 0x45, 0x92, 0x31, 0xa8, 0xf0, 0x77, 0x16, 0x7a, - 0x6d, 0x4c, 0x1e, 0xf1, 0x46, 0x46, 0xd0, 0x6c, 0xed, 0xb5, 0x37, 0x4f, 0x63, 0x0a, 0x20, 0x65, - 0x05, 0xf2, 0x36, 0xde, 0x30, 0x41, 0x3c, 0x26, 0x61, 0xf0, 0x85, 0xcd, 0x41, 0x63, 0x9f, 0xb7, - 0xc9, 0x01, 0x74, 0xd8, 0x10, 0x3f, 0xb7, 0x50, 0x31, 0x4b, 0xbb, 0xb0, 0x7b, 0xc2, 0x73, 0xc7, - 0xe4, 0xd8, 0x26, 0xa7, 0xb6, 0x07, 0xd8, 0x1d, 0x05, 0x5b, 0xc6, 0x64, 0x1a, 0x6c, 0x2c, 0x92, - 0xe4, 0x20, 0xbe, 0x1a, 0xe2, 0xdf, 0x2c, 0xb4, 0x90, 0x23, 0xb7, 0xb8, 0x7c, 0x4a, 0x8a, 0x97, - 0x2c, 0xf0, 0xfb, 0x8a, 0x79, 0x17, 0xdf, 0x3e, 0x0d, 0xb3, 0xae, 0xf4, 0x88, 0x3b, 0x55, 0xf4, - 0xef, 0x2d, 0x74, 0xc1, 0x10, 0x66, 0x7c, 0x35, 0x1b, 0x62, 0x42, 0xf5, 0xed, 0xf5, 0x93, 0x0d, - 0x81, 0xf5, 0x96, 0x62, 0xbd, 0x81, 0xaf, 0x11, 0xc9, 0x3a, 0x1d, 0x11, 0x6c, 0x71, 0x31, 0x49, - 0xdd, 0xd2, 0x7e, 0xd1, 0xe7, 0x0d, 0xf0, 0x45, 0xfa, 0x34, 0xc4, 0xbf, 0xe8, 0xa6, 0x4d, 0x0b, - 0x6d, 0x5e, 0xd3, 0x66, 0x0c, 0x86, 0xbc, 0x9a, 0x66, 0xc9, 0xbd, 0x73, 0x5f, 0x71, 0xde, 0xc5, - 0xbb, 0xd3, 0x38, 0xe3, 0x6f, 0xb0, 0x86, 0x12, 0x7b, 0x72, 0xa0, 0x07, 0xcd, 0xd0, 0x60, 0xfe, - 0xd5, 0x42, 0x17, 0x33, 0x54, 0x1b, 0x5f, 0x9f, 0x5a, 0xb2, 0x9c, 0x09, 0x61, 0xe7, 0xf4, 0x7f, - 0xde, 0x4c, 0x70, 0x76, 0x55, 0x1a, 0x37, 0xf1, 0xce, 0xb4, 0x34, 0x46, 0x5f, 0x8b, 0x6d, 0xe8, - 0x92, 0x54, 0x53, 0xfc, 0xac, 0x9b, 0x22, 0x99, 0x0f, 0x79, 0x4d, 0x31, 0x31, 0x41, 0xfe, 0x37, - 0x6b, 0x55, 0xb1, 0xde, 0xc6, 0xb7, 0xf2, 0x58, 0x8d, 0xa7, 0x24, 0x8c, 0x51, 0xdd, 0x61, 0xfa, - 0x0c, 0xf1, 0x4f, 0xba, 0xdc, 0xe3, 0x63, 0x05, 0x6f, 0x65, 0xc3, 0xe4, 0x8c, 0xa8, 0x3c, 0xf6, - 0xbc, 0x69, 0xe5, 0xbc, 0xab, 0xd8, 0xdf, 0xc1, 0xee, 0xb4, 0x3a, 0x47, 0x98, 0x8d, 0x68, 0x7c, - 0x45, 0x75, 0xd6, 0x58, 0x20, 0xc3, 0xe9, 0xf9, 0x90, 0xd7, 0xd1, 0x19, 0xc3, 0x29, 0xaf, 0xa3, - 0xb3, 0x06, 0x55, 0x4a, 0x86, 0xa7, 0x20, 0x1a, 0xdf, 0xf9, 0x95, 0xea, 0x8b, 0xa3, 0x92, 0x75, - 0x78, 0x54, 0xb2, 0xfe, 0x3e, 0x2a, 0x59, 0x5f, 0x1f, 0x97, 0x66, 0x0e, 0x8f, 0x4b, 0x33, 0x7f, - 0x1c, 0x97, 0x66, 0x3e, 0xde, 0x48, 0x8d, 0xda, 0xf1, 0x70, 0x5f, 0x8c, 0x7e, 0xbb, 0x44, 0x13, - 0xb7, 0x59, 0x50, 0xbf, 0x16, 0xae, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0xe3, 0x75, 0x55, 0x5b, - 0xb8, 0x0d, 0x00, 0x00, + // 1036 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x96, 0xcf, 0x6f, 0x1b, 0x45, + 0x14, 0xc7, 0xb3, 0x69, 0x71, 0xc9, 0xa4, 0x08, 0x31, 0x35, 0x4d, 0xb2, 0x8d, 0x9c, 0x64, 0x91, + 0xda, 0x24, 0x28, 0x3b, 0x38, 0x6d, 0x09, 0x55, 0x55, 0xa2, 0xda, 0x45, 0x55, 0x0f, 0x48, 0xd4, + 0x71, 0x25, 0x04, 0x07, 0x6b, 0x6c, 0x0f, 0xdb, 0x51, 0x6d, 0xcf, 0x66, 0x77, 0x0c, 0x58, 0x91, + 0x2f, 0x5c, 0xb9, 0x20, 0x71, 0xe4, 0xc0, 0x15, 0x21, 0x90, 0xca, 0x1d, 0x89, 0x6b, 0x8f, 0x91, + 0xb8, 0x70, 0x02, 0x94, 0x20, 0xf1, 0x47, 0x70, 0x41, 0x33, 0xf3, 0xd6, 0xeb, 0xb1, 0x77, 0x9d, + 0xc0, 0x25, 0xd9, 0x9d, 0xf7, 0x63, 0x3f, 0xef, 0xcd, 0xf3, 0x77, 0x06, 0x2d, 0x77, 0xe8, 0x80, + 0x45, 0x44, 0x44, 0xb4, 0xd5, 0x61, 0xe4, 0xb0, 0xcf, 0xa2, 0x81, 0x1f, 0x46, 0x42, 0x0a, 0x7c, + 0x59, 0x5b, 0x7c, 0x63, 0x71, 0x8b, 0x81, 0x08, 0x84, 0x36, 0x10, 0xf5, 0x64, 0x7c, 0xdc, 0xd5, + 0x40, 0x88, 0xa0, 0xc3, 0x08, 0x0d, 0x39, 0xa1, 0xbd, 0x9e, 0x90, 0x54, 0x72, 0xd1, 0x8b, 0xc1, + 0xba, 0xdd, 0x12, 0x71, 0x57, 0xc4, 0xa4, 0x49, 0x63, 0x48, 0x4d, 0x3e, 0x2d, 0x37, 0x99, 0xa4, + 0x65, 0x12, 0xd2, 0x80, 0xf7, 0xb4, 0x33, 0xf8, 0xae, 0x58, 0x1c, 0x21, 0x8d, 0x68, 0x37, 0x49, + 0xe3, 0x5a, 0xa6, 0x88, 0x85, 0x22, 0x92, 0x89, 0x6d, 0xcd, 0xb2, 0x75, 0x79, 0x2b, 0x12, 0x0d, + 0xe3, 0x01, 0x0e, 0x4b, 0x96, 0x83, 0xe4, 0x61, 0x12, 0x79, 0xcd, 0x32, 0xf4, 0x63, 0x16, 0x35, + 0x24, 0x0f, 0x93, 0xba, 0x2c, 0x23, 0x0d, 0x82, 0x88, 0x05, 0x54, 0x32, 0xb0, 0xbe, 0x46, 0xbb, + 0xbc, 0x27, 0x88, 0xfe, 0x0b, 0x4b, 0xa5, 0xf1, 0x52, 0x93, 0x22, 0x5b, 0x82, 0x43, 0x79, 0x5e, + 0x11, 0xe1, 0xc7, 0xaa, 0x01, 0x1f, 0xe8, 0xc2, 0x6a, 0xec, 0xb0, 0xcf, 0x62, 0xe9, 0x3d, 0x42, + 0x57, 0xac, 0xd5, 0x38, 0x14, 0xbd, 0x98, 0xe1, 0x5d, 0x54, 0x30, 0x0d, 0x58, 0x76, 0xd6, 0x9d, + 0xcd, 0xc5, 0xdd, 0xa2, 0x3f, 0xbe, 0x15, 0xbe, 0xf1, 0xae, 0x5c, 0x7c, 0xf1, 0xfb, 0xda, 0x5c, + 0x0d, 0x3c, 0xbd, 0x3d, 0x74, 0x4d, 0xa7, 0x7a, 0xc8, 0x64, 0xcd, 0x74, 0xa8, 0x39, 0x78, 0xcc, + 0xdb, 0xf0, 0x25, 0xbc, 0x8c, 0x2e, 0xe9, 0x0d, 0x78, 0xd4, 0xd6, 0x39, 0x17, 0x6a, 0xc9, 0xab, + 0xf7, 0x04, 0xad, 0x66, 0x07, 0x02, 0xcc, 0x6d, 0x74, 0x09, 0x5a, 0x0e, 0x34, 0xaf, 0xdb, 0x34, + 0x10, 0x04, 0x38, 0x89, 0xaf, 0xf7, 0x2e, 0x5a, 0x9f, 0x4a, 0x6b, 0x1e, 0x58, 0x94, 0x40, 0xb9, + 0xe8, 0xe5, 0x08, 0x96, 0x80, 0x6a, 0xf4, 0xee, 0x3d, 0x45, 0x1b, 0x33, 0xe2, 0x81, 0xad, 0x8a, + 0x2e, 0xeb, 0x2d, 0xaf, 0x8d, 0x00, 0x2f, 0x6c, 0x2e, 0xee, 0xae, 0xd8, 0x80, 0xef, 0xa7, 0x1e, + 0x00, 0x69, 0x05, 0x79, 0x1f, 0xa3, 0x37, 0x72, 0xbf, 0x34, 0xd6, 0xc1, 0x19, 0xb0, 0xe3, 0xdd, + 0x9d, 0xb7, 0xbb, 0x7b, 0x07, 0xad, 0x24, 0xc9, 0xab, 0xfd, 0x28, 0x62, 0x3d, 0x59, 0xe7, 0x61, + 0x92, 0x72, 0x15, 0x2d, 0x68, 0xbf, 0x07, 0x54, 0x52, 0xc8, 0x99, 0x2e, 0x78, 0x0f, 0x90, 0x9b, + 0x15, 0x0a, 0xa5, 0x5f, 0x47, 0x17, 0xd5, 0x30, 0xc3, 0x9e, 0x60, 0xbb, 0xe4, 0x3a, 0x0f, 0xe3, + 0x9a, 0xb6, 0x7b, 0x07, 0xe9, 0x5c, 0x3c, 0x89, 0x59, 0x54, 0xe7, 0x61, 0x5d, 0x48, 0xda, 0x49, + 0x10, 0xae, 0xa2, 0x82, 0xe4, 0x61, 0x38, 0xaa, 0x09, 0xde, 0x6c, 0xb4, 0xf9, 0x49, 0xb4, 0x0f, + 0xd3, 0x99, 0xb1, 0x93, 0x02, 0xdc, 0x3b, 0x68, 0x41, 0xaa, 0x85, 0x7a, 0x4a, 0xe8, 0xda, 0x84, + 0x56, 0x58, 0xea, 0xec, 0x55, 0xd0, 0xf5, 0x89, 0xa2, 0xef, 0x27, 0x3f, 0xbe, 0xb6, 0xd9, 0x94, + 0xb3, 0x27, 0xfa, 0x20, 0x1d, 0xbd, 0xe9, 0x60, 0x20, 0x24, 0xa8, 0x60, 0x76, 0x0f, 0xf0, 0x96, + 0x6c, 0xbc, 0x51, 0x5c, 0x0d, 0xdc, 0xbc, 0x83, 0x74, 0x23, 0x55, 0x0b, 0x2a, 0xec, 0x13, 0x11, + 0xb1, 0x33, 0x59, 0x54, 0x1f, 0x25, 0xef, 0xb2, 0x58, 0xd2, 0x6e, 0xa8, 0xfb, 0x78, 0xa1, 0x96, + 0x2e, 0x78, 0x1b, 0x68, 0x2d, 0x49, 0x5a, 0xe7, 0x5d, 0x56, 0xa1, 0xb1, 0x02, 0xfd, 0x8c, 0x46, + 0xed, 0x91, 0x44, 0x7c, 0xe9, 0xa4, 0xd5, 0x4c, 0xfb, 0x40, 0x35, 0x81, 0xaa, 0x46, 0x2d, 0x41, + 0x35, 0x2b, 0xbe, 0x91, 0x23, 0x5f, 0xc9, 0x91, 0x0f, 0x72, 0xe4, 0x57, 0x05, 0xef, 0x55, 0x6e, + 0xa9, 0x5f, 0xc0, 0xf7, 0x7f, 0xac, 0xdd, 0x08, 0xb8, 0x7c, 0xda, 0x6f, 0xfa, 0x2d, 0xd1, 0x25, + 0xa0, 0x5d, 0xe6, 0xdf, 0x4e, 0xdc, 0x7e, 0x46, 0xe4, 0x20, 0x64, 0xb1, 0x0e, 0xf8, 0xee, 0xef, + 0xe7, 0xdb, 0x4e, 0x0d, 0xd2, 0xef, 0xfe, 0xb3, 0x88, 0x5e, 0xd2, 0x34, 0xf8, 0x19, 0x2a, 0x18, + 0x1d, 0xc2, 0xeb, 0x76, 0xeb, 0xa6, 0x65, 0xce, 0xdd, 0x98, 0xe1, 0x61, 0x2a, 0xf0, 0x56, 0xbf, + 0xf8, 0xf5, 0xaf, 0xaf, 0xe7, 0xaf, 0xe2, 0x22, 0xc9, 0x38, 0x07, 0xf0, 0x37, 0x0e, 0x7a, 0x75, + 0x42, 0x9f, 0xf0, 0x56, 0x46, 0xd2, 0x6c, 0xf1, 0x73, 0xb7, 0xcf, 0xe3, 0x0a, 0x20, 0x65, 0x0d, + 0xf2, 0x26, 0xde, 0xb2, 0x41, 0x02, 0x26, 0xe1, 0x5c, 0x89, 0x9b, 0x83, 0xc6, 0x21, 0x6f, 0x93, + 0x23, 0xd8, 0xe2, 0x21, 0x7e, 0xee, 0xa0, 0x62, 0x96, 0x78, 0x60, 0xff, 0x8c, 0xef, 0x4e, 0xe8, + 0xa1, 0x4b, 0xce, 0xed, 0x0f, 0xb0, 0x7b, 0x1a, 0xb6, 0x8c, 0xc9, 0x2c, 0xd8, 0x44, 0xa5, 0xc8, + 0x51, 0xf2, 0x34, 0xc4, 0xbf, 0x38, 0x68, 0x29, 0x47, 0xef, 0x70, 0xf9, 0x9c, 0x14, 0xff, 0xb3, + 0xc1, 0xef, 0x69, 0xe6, 0x7d, 0x7c, 0xef, 0x3c, 0xcc, 0xa6, 0xd3, 0x23, 0xee, 0xb1, 0xa6, 0x7f, + 0xeb, 0xa0, 0x57, 0x2c, 0x65, 0xc4, 0x37, 0xb2, 0x21, 0xa6, 0x64, 0xd7, 0xdd, 0x3c, 0xdb, 0x11, + 0x58, 0xef, 0x6a, 0xd6, 0xdb, 0xf8, 0x26, 0x91, 0xac, 0xd3, 0x11, 0xd1, 0x0e, 0x17, 0xd3, 0xd4, + 0x2d, 0x13, 0xa7, 0x6e, 0x0f, 0xc0, 0xa7, 0x04, 0x62, 0x88, 0x7f, 0x32, 0x43, 0x3b, 0xae, 0x74, + 0x79, 0x43, 0x9b, 0xa1, 0xcc, 0x79, 0x3d, 0xcd, 0xd2, 0x5b, 0xef, 0xa1, 0xe6, 0xbc, 0x8f, 0xf7, + 0x67, 0x71, 0x26, 0x57, 0x9c, 0x86, 0x56, 0x5b, 0x72, 0x64, 0x94, 0x7e, 0x68, 0x31, 0xff, 0xec, + 0xa0, 0x2b, 0x19, 0xb2, 0x89, 0x6f, 0xcd, 0x6c, 0x59, 0x8e, 0x44, 0xbb, 0x39, 0xf3, 0x9f, 0x27, + 0xca, 0xde, 0xbe, 0x2e, 0xe3, 0x0e, 0xde, 0x9b, 0x55, 0xc6, 0xe8, 0x32, 0xd6, 0x86, 0x29, 0x19, + 0x1b, 0x8a, 0x1f, 0xcd, 0x50, 0xa4, 0x02, 0x9d, 0x37, 0x14, 0x53, 0x12, 0xfe, 0x9f, 0x59, 0xab, + 0x9a, 0xf5, 0x1e, 0xbe, 0x9b, 0xc7, 0x6a, 0x7d, 0x25, 0x65, 0x54, 0x7d, 0x07, 0xf9, 0x1f, 0xe2, + 0x1f, 0x4c, 0xbb, 0x27, 0x75, 0x1d, 0xef, 0x64, 0xc3, 0xe4, 0x9c, 0x11, 0x79, 0xec, 0x79, 0xc7, + 0x85, 0xf7, 0xb6, 0x66, 0x7f, 0x0b, 0xfb, 0xb3, 0xfa, 0xac, 0x30, 0x1b, 0xea, 0xfc, 0x50, 0x7d, + 0xd6, 0xf1, 0x95, 0xea, 0x8b, 0x93, 0x92, 0x73, 0x7c, 0x52, 0x72, 0xfe, 0x3c, 0x29, 0x39, 0x5f, + 0x9d, 0x96, 0xe6, 0x8e, 0x4f, 0x4b, 0x73, 0xbf, 0x9d, 0x96, 0xe6, 0x3e, 0xda, 0x1a, 0x3b, 0x4d, + 0x26, 0x73, 0x7e, 0x3e, 0xba, 0x7c, 0xab, 0x43, 0xa5, 0x59, 0xd0, 0x17, 0xe2, 0x9b, 0xff, 0x06, + 0x00, 0x00, 0xff, 0xff, 0x8c, 0xe6, 0x8a, 0x07, 0x79, 0x0c, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -954,8 +867,6 @@ type QueryClient interface { GetDataBefore(ctx context.Context, in *QueryGetDataBeforeRequest, opts ...grpc.CallOption) (*QueryGetAggregatedReportResponse, error) // Queries a list of GetTimeBasedRewards items. GetTimeBasedRewards(ctx context.Context, in *QueryGetTimeBasedRewardsRequest, opts ...grpc.CallOption) (*QueryGetTimeBasedRewardsResponse, error) - // Queries a list of GetMedianValues items. - GetMedianValues(ctx context.Context, in *QueryGetMedianValuesRequest, opts ...grpc.CallOption) (*QueryGetMedianValuesResponse, error) } type queryClient struct { @@ -1047,15 +958,6 @@ func (c *queryClient) GetTimeBasedRewards(ctx context.Context, in *QueryGetTimeB return out, nil } -func (c *queryClient) GetMedianValues(ctx context.Context, in *QueryGetMedianValuesRequest, opts ...grpc.CallOption) (*QueryGetMedianValuesResponse, error) { - out := new(QueryGetMedianValuesResponse) - err := c.cc.Invoke(ctx, "/layer.oracle.Query/GetMedianValues", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - // QueryServer is the server API for Query service. type QueryServer interface { // Parameters queries the parameters of the module. @@ -1074,8 +976,6 @@ type QueryServer interface { GetDataBefore(context.Context, *QueryGetDataBeforeRequest) (*QueryGetAggregatedReportResponse, error) // Queries a list of GetTimeBasedRewards items. GetTimeBasedRewards(context.Context, *QueryGetTimeBasedRewardsRequest) (*QueryGetTimeBasedRewardsResponse, error) - // Queries a list of GetMedianValues items. - GetMedianValues(context.Context, *QueryGetMedianValuesRequest) (*QueryGetMedianValuesResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -1109,9 +1009,6 @@ func (*UnimplementedQueryServer) GetDataBefore(ctx context.Context, req *QueryGe func (*UnimplementedQueryServer) GetTimeBasedRewards(ctx context.Context, req *QueryGetTimeBasedRewardsRequest) (*QueryGetTimeBasedRewardsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetTimeBasedRewards not implemented") } -func (*UnimplementedQueryServer) GetMedianValues(ctx context.Context, req *QueryGetMedianValuesRequest) (*QueryGetMedianValuesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetMedianValues not implemented") -} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -1279,24 +1176,6 @@ func _Query_GetTimeBasedRewards_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } -func _Query_GetMedianValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(QueryGetMedianValuesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(QueryServer).GetMedianValues(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/layer.oracle.Query/GetMedianValues", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(QueryServer).GetMedianValues(ctx, req.(*QueryGetMedianValuesRequest)) - } - return interceptor(ctx, in, info, handler) -} - var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "layer.oracle.Query", HandlerType: (*QueryServer)(nil), @@ -1337,10 +1216,6 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "GetTimeBasedRewards", Handler: _Query_GetTimeBasedRewards_Handler, }, - { - MethodName: "GetMedianValues", - Handler: _Query_GetMedianValues_Handler, - }, }, Streams: []grpc.StreamDesc{}, Metadata: "layer/oracle/query.proto", @@ -1862,66 +1737,6 @@ func (m *QueryGetTimeBasedRewardsResponse) MarshalToSizedBuffer(dAtA []byte) (in return len(dAtA) - i, nil } -func (m *QueryGetMedianValuesRequest) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *QueryGetMedianValuesRequest) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *QueryGetMedianValuesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - return len(dAtA) - i, nil -} - -func (m *QueryGetMedianValuesResponse) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *QueryGetMedianValuesResponse) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *QueryGetMedianValuesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.MedianValues) > 0 { - for iNdEx := len(m.MedianValues) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.MedianValues[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintQuery(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -2140,30 +1955,6 @@ func (m *QueryGetTimeBasedRewardsResponse) Size() (n int) { return n } -func (m *QueryGetMedianValuesRequest) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - return n -} - -func (m *QueryGetMedianValuesResponse) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.MedianValues) > 0 { - for _, e := range m.MedianValues { - l = e.Size() - n += 1 + l + sovQuery(uint64(l)) - } - } - return n -} - func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -3518,140 +3309,6 @@ func (m *QueryGetTimeBasedRewardsResponse) Unmarshal(dAtA []byte) error { } return nil } -func (m *QueryGetMedianValuesRequest) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: QueryGetMedianValuesRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: QueryGetMedianValuesRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - default: - iNdEx = preIndex - skippy, err := skipQuery(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthQuery - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *QueryGetMedianValuesResponse) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: QueryGetMedianValuesResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: QueryGetMedianValuesResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field MedianValues", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthQuery - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthQuery - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.MedianValues = append(m.MedianValues, &MedianValues{}) - if err := m.MedianValues[len(m.MedianValues)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipQuery(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthQuery - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/oracle/types/query.pb.gw.go b/x/oracle/types/query.pb.gw.go index 8d667df36..f074f54b1 100644 --- a/x/oracle/types/query.pb.gw.go +++ b/x/oracle/types/query.pb.gw.go @@ -513,24 +513,6 @@ func local_request_Query_GetTimeBasedRewards_0(ctx context.Context, marshaler ru } -func request_Query_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq QueryGetMedianValuesRequest - var metadata runtime.ServerMetadata - - msg, err := client.GetMedianValues(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_Query_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq QueryGetMedianValuesRequest - var metadata runtime.ServerMetadata - - msg, err := server.GetMedianValues(ctx, &protoReq) - return msg, metadata, err - -} - // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -744,29 +726,6 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) - mux.Handle("GET", pattern_Query_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_Query_GetMedianValues_0(rctx, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Query_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - return nil } @@ -988,26 +947,6 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) - mux.Handle("GET", pattern_Query_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_Query_GetMedianValues_0(rctx, inboundMarshaler, client, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Query_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - return nil } @@ -1029,8 +968,6 @@ var ( pattern_Query_GetDataBefore_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5}, []string{"tellor-io", "layer", "oracle", "GetDataBefore", "queryId", "timestamp"}, "", runtime.AssumeColonVerbOpt(true))) pattern_Query_GetTimeBasedRewards_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"tellor-io", "layer", "oracle", "get_time_based_rewards"}, "", runtime.AssumeColonVerbOpt(true))) - - pattern_Query_GetMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"tellor-io", "layer", "oracle", "get_median_values"}, "", runtime.AssumeColonVerbOpt(true))) ) var ( @@ -1051,6 +988,4 @@ var ( forward_Query_GetDataBefore_0 = runtime.ForwardResponseMessage forward_Query_GetTimeBasedRewards_0 = runtime.ForwardResponseMessage - - forward_Query_GetMedianValues_0 = runtime.ForwardResponseMessage ) From f7ccbcb1dd99759cdde7358bc5bb589a94b49aa7 Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 17 Jan 2024 09:30:16 -0500 Subject: [PATCH 03/10] renamed --- cmd/layerd/cmd/{price.go => root_option.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/layerd/cmd/{price.go => root_option.go} (100%) diff --git a/cmd/layerd/cmd/price.go b/cmd/layerd/cmd/root_option.go similarity index 100% rename from cmd/layerd/cmd/price.go rename to cmd/layerd/cmd/root_option.go From 3e515350c4d68e182b5fa6b0f40f0ef995253fce Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 17 Jan 2024 12:10:17 -0500 Subject: [PATCH 04/10] add submit and commit from api --- x/oracle/client/cli/tx.go | 1 + .../client/cli/tx_commit_submit_report.go | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 x/oracle/client/cli/tx_commit_submit_report.go diff --git a/x/oracle/client/cli/tx.go b/x/oracle/client/cli/tx.go index cd65c7b9d..f05a14433 100644 --- a/x/oracle/client/cli/tx.go +++ b/x/oracle/client/cli/tx.go @@ -33,6 +33,7 @@ func GetTxCmd() *cobra.Command { cmd.AddCommand(CmdSubmitValue()) cmd.AddCommand(CmdCommitReport()) cmd.AddCommand(CmdTip()) + cmd.AddCommand(CmdCommitSubmitReport()) // this line is used by starport scaffolding # 1 return cmd diff --git a/x/oracle/client/cli/tx_commit_submit_report.go b/x/oracle/client/cli/tx_commit_submit_report.go new file mode 100644 index 000000000..63a10d60b --- /dev/null +++ b/x/oracle/client/cli/tx_commit_submit_report.go @@ -0,0 +1,129 @@ +package cli + +import ( + "encoding/hex" + "fmt" + "math/big" + "strconv" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/spf13/cobra" + mediantypes "github.com/tellor-io/layer/daemons/server/types" + "github.com/tellor-io/layer/x/oracle/types" +) + +var _ = strconv.Itoa(0) + +var ( + queryDataIdMap = map[string]uint32{ + "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003627463000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000": 0, + "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003657468000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000": 1, + } +) + +func EncodeValue(number float64) (string, error) { + strNumber := fmt.Sprintf("%.18f", number) + + parts := strings.Split(strNumber, ".") + if len(parts[1]) > 18 { + parts[1] = parts[1][:18] + } + truncatedStr := parts[0] + parts[1] + + bigIntNumber := new(big.Int) + _, ok := bigIntNumber.SetString(truncatedStr, 10) + if !ok { + return "", fmt.Errorf("error converting string to big int") + } + + uint256ABIType, err := abi.NewType("uint256", "", nil) + if err != nil { + return "", fmt.Errorf("error creating uint256 abi type, %v", err) + } + + arguments := abi.Arguments{{Type: uint256ABIType}} + encodedBytes, err := arguments.Pack(bigIntNumber) + if err != nil { + return "", fmt.Errorf("error packing arguments, %v", err) + } + + encodedString := hex.EncodeToString(encodedBytes) + return encodedString, nil +} +func CmdCommitSubmitReport() *cobra.Command { + cmd := &cobra.Command{ + Use: "commit-submit-report [query-data]", + Short: "Broadcast message report", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + argQueryData := args[0] + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + queryClient := mediantypes.NewMedianValuesServiceClient(clientCtx) + + params := &mediantypes.GetMedianValuesRequest{} + + medianValue, err := queryClient.GetMedianValues(cmd.Context(), params) + if err != nil { + return err + } + if len(medianValue.MedianValues) == 0 { + return fmt.Errorf("no median values found") + } + var hexValue string + for _, value := range medianValue.MedianValues { + if queryDataIdMap[argQueryData] == value.MarketId { + fmt.Println("Spot Price:", value.Price) + hexValue, err = EncodeValue(float64(value.Price)) + if err != nil { + return err + } + break + } + return fmt.Errorf("query data not found in median values") + } + valueDecoded, err := hex.DecodeString(hexValue) + if err != nil { + return err + } + data, _, err := clientCtx.Keyring.SignByAddress(clientCtx.GetFromAddress(), valueDecoded) + if err != nil { + return err + } + + msg := types.NewMsgCommitReport( + clientCtx.GetFromAddress().String(), + argQueryData, + hex.EncodeToString(data), + ) + if err := msg.ValidateBasic(); err != nil { + return err + } + if err = tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg); err != nil { + panic(err) + } + time.Sleep(time.Second) + msgSubmit := types.NewMsgSubmitValue( + clientCtx.GetFromAddress().String(), + argQueryData, + hexValue, + ) + if err := msgSubmit.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msgSubmit) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} From 44717b26221693badcd696ae917eb781d8b9d5f3 Mon Sep 17 00:00:00 2001 From: akrem Date: Thu, 18 Jan 2024 20:06:43 -0500 Subject: [PATCH 05/10] add test and metrics/health_checker daemon --- app/app.go | 66 +- .../configs/default_market_param_config.go | 2 +- ...o => default_pricefeed_exchange_config.go} | 2 +- ...default_pricefeed_exchange_config_test.go} | 0 daemons/metrics/client/client.go | 61 ++ daemons/pricefeed/client/client.go | 6 +- daemons/pricefeed/client/client_test.go | 918 ++++++++++++++++++ .../price_encoder/mutable_state_test.go | 132 +++ .../price_encoder/price_encoder_test.go | 420 ++++++++ daemons/server/server.go | 6 - daemons/server/types/health_checker.go | 228 +++++ daemons/server/types/health_monitor.go | 239 +++++ mocks/ExchangeToMarketPrices.go | 74 ++ mocks/GrpcClient.go | 91 ++ mocks/Logger.go | 70 ++ mocks/PriceFeedMutableMarketConfigs.go | 108 +++ mocks/QueryClient.go | 69 ++ testutil/appoptions/app_options.go | 72 ++ .../client/exchange_id_to_price_timestamp.go | 13 + testutil/constants/price_feed.go | 689 +++++++++++++ testutil/constants/prices.go | 214 +++- testutil/constants/time.go | 23 + testutil/daemons/common.go | 32 + .../exchange_config/static_market_names.go | 46 + testutil/daemons/pricefeed/pricefeed_utils.go | 49 +- testutil/encoding/utils.go | 67 ++ testutil/grpc/grpc.go | 48 + testutil/grpc/tcp.go | 35 + 28 files changed, 3755 insertions(+), 25 deletions(-) rename daemons/configs/{default_config.go => default_pricefeed_exchange_config.go} (98%) rename daemons/configs/{default_config_test.go => default_pricefeed_exchange_config_test.go} (100%) create mode 100644 daemons/metrics/client/client.go create mode 100644 daemons/pricefeed/client/client_test.go create mode 100644 daemons/pricefeed/client/price_encoder/mutable_state_test.go create mode 100644 daemons/pricefeed/client/price_encoder/price_encoder_test.go create mode 100644 daemons/server/types/health_checker.go create mode 100644 daemons/server/types/health_monitor.go create mode 100644 mocks/ExchangeToMarketPrices.go create mode 100644 mocks/GrpcClient.go create mode 100644 mocks/Logger.go create mode 100644 mocks/PriceFeedMutableMarketConfigs.go create mode 100644 mocks/QueryClient.go create mode 100644 testutil/appoptions/app_options.go create mode 100644 testutil/client/exchange_id_to_price_timestamp.go create mode 100644 testutil/constants/price_feed.go create mode 100644 testutil/constants/time.go create mode 100644 testutil/daemons/common.go create mode 100644 testutil/daemons/pricefeed/exchange_config/static_market_names.go create mode 100644 testutil/encoding/utils.go create mode 100644 testutil/grpc/grpc.go create mode 100644 testutil/grpc/tcp.go diff --git a/app/app.go b/app/app.go index c99676223..ce686b0c8 100644 --- a/app/app.go +++ b/app/app.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "time" autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" @@ -132,9 +133,13 @@ import ( "github.com/tellor-io/layer/daemons/configs" "github.com/tellor-io/layer/daemons/constants" daemonflags "github.com/tellor-io/layer/daemons/flags" + metricsclient "github.com/tellor-io/layer/daemons/metrics/client" daemonserver "github.com/tellor-io/layer/daemons/server" + daemonservertypes "github.com/tellor-io/layer/daemons/server/types" daemontypes "github.com/tellor-io/layer/daemons/types" + "runtime/debug" + pricefeedclient "github.com/tellor-io/layer/daemons/pricefeed/client" medianserver "github.com/tellor-io/layer/daemons/server/median" pricefeedtypes "github.com/tellor-io/layer/daemons/server/types/pricefeed" @@ -295,9 +300,10 @@ type App struct { sm *module.SimulationManager configurator module.Configurator - Server *daemonserver.Server - startDaemons func(client.Context, *api.Server) - PriceFeedClient *pricefeedclient.Client + Server *daemonserver.Server + startDaemons func(client.Context, *api.Server) + PriceFeedClient *pricefeedclient.Client + DaemonHealthMonitor *daemonservertypes.HealthMonitor } // New returns a reference to an initialized blockchain app @@ -615,12 +621,19 @@ func New( daemonFlags.Shared.SocketAddress, ) app.Server.WithPriceFeedMarketToExchangePrices(indexPriceCache) + app.DaemonHealthMonitor = daemonservertypes.NewHealthMonitor( + daemonservertypes.DaemonStartupGracePeriod, + daemonservertypes.HealthCheckPollFrequency, + app.Logger(), + daemonFlags.Shared.PanicOnDaemonFailureEnabled, + ) // Create a closure for starting daemons and daemon server. Daemon services are delayed until after the gRPC // service is started because daemons depend on the gRPC service being available. If a node is initialized // with a genesis time in the future, then the gRPC service will not be available until the genesis time, the // daemons will not be able to connect to the cosmos gRPC query service and finish initialization, and the daemon // monitoring service will panic. app.startDaemons = func(cltx client.Context, apiSvr *api.Server) { + maxDaemonUnhealthyDuration := time.Duration(daemonFlags.Shared.MaxDaemonUnhealthySeconds) * time.Second // Start server for handling gRPC messages from daemons. go app.Server.Start() @@ -645,8 +658,35 @@ func New( &pricefeedclient.SubTaskRunnerImpl{}, ) medianserver.StartMedianServer(cltx, app.GRPCQueryRouter(), apiSvr.GRPCGatewayRouter, marketParamsConfig, indexPriceCache) + app.RegisterDaemonWithHealthMonitor(app.PriceFeedClient, maxDaemonUnhealthyDuration) } + // Start the Metrics Daemon. + // The metrics daemon is purely used for observability. It should never bring the app down. + // TODO(CLOB-960) Don't start this goroutine if telemetry is disabled + // Note: the metrics daemon is such a simple go-routine that we don't bother implementing a health-check + // for this service. The task loop does not produce any errors because the telemetry calls themselves are + // not error-returning, so in effect this daemon would never become unhealthy. + go func() { + defer func() { + if r := recover(); r != nil { + logger.Error( + "Metrics Daemon exited unexpectedly with a panic.", + "panic", + r, + "stack", + string(debug.Stack()), + ) + } + }() + metricsclient.Start( + // The client will use `context.Background` so that it can have a different context from + // the main application. + context.Background(), + logger, + ) + }() } + /**** IBC Routing ****/ // Sealing prevents other modules from creating scoped sub-keepers @@ -1050,3 +1090,23 @@ func (app *App) SimulationManager() *module.SimulationManager { func (app *App) ModuleManager() *module.Manager { return app.mm } + +// 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( + healthCheckableDaemon daemontypes.HealthCheckable, + maxDaemonUnhealthyDuration time.Duration, +) { + if err := app.DaemonHealthMonitor.RegisterService(healthCheckableDaemon, maxDaemonUnhealthyDuration); err != nil { + app.Logger().Error( + "Failed to register daemon service with update monitor", + "error", + err, + "service", + healthCheckableDaemon.ServiceName(), + "maxDaemonUnhealthyDuration", + maxDaemonUnhealthyDuration, + ) + panic(err) + } +} diff --git a/daemons/configs/default_market_param_config.go b/daemons/configs/default_market_param_config.go index 5eb8020d7..893fb0967 100644 --- a/daemons/configs/default_market_param_config.go +++ b/daemons/configs/default_market_param_config.go @@ -70,7 +70,7 @@ func ReadMarketParamsConfigFile(homeDir string) []types.MarketParam { // Read file for exchange query configurations. tomlFile, err := os.ReadFile(getMarketParamsConfigFilePath(homeDir)) if err != nil { - panic(fmt.Errorf("failed to read exchange query config file: %v, %v", tomlFile, err)) + panic(err) } params := map[string][]types.MarketParam{} diff --git a/daemons/configs/default_config.go b/daemons/configs/default_pricefeed_exchange_config.go similarity index 98% rename from daemons/configs/default_config.go rename to daemons/configs/default_pricefeed_exchange_config.go index 7bb5e17f2..4aa917a99 100644 --- a/daemons/configs/default_config.go +++ b/daemons/configs/default_pricefeed_exchange_config.go @@ -76,7 +76,7 @@ func ReadExchangeQueryConfigFile(homeDir string) map[types.ExchangeId]*types.Exc // Read file for exchange query configurations. tomlFile, err := os.ReadFile(getConfigFilePath(homeDir)) if err != nil { - panic(fmt.Errorf("failed to read exchange query config file: %v, %v", tomlFile, err)) + panic(err) } // Unmarshal `tomlFile` into `exchanges` for `exchangeStartupConfigMap`. diff --git a/daemons/configs/default_config_test.go b/daemons/configs/default_pricefeed_exchange_config_test.go similarity index 100% rename from daemons/configs/default_config_test.go rename to daemons/configs/default_pricefeed_exchange_config_test.go diff --git a/daemons/metrics/client/client.go b/daemons/metrics/client/client.go new file mode 100644 index 000000000..372b5d7e8 --- /dev/null +++ b/daemons/metrics/client/client.go @@ -0,0 +1,61 @@ +package client + +import ( + "context" + "time" + + gometrics "github.com/armon/go-metrics" + "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/cosmos-sdk/telemetry" + "github.com/cosmos/cosmos-sdk/version" + "github.com/tellor-io/layer/lib/metrics" +) + +var ( + // 30 minutes + METRICS_DAEMON_LOOP_DELAY_MS uint32 = 30 * 60 * 1000 + METRICS_DAEMON_LOOP_DELAY_DURATION = time.Duration(METRICS_DAEMON_LOOP_DELAY_MS) +) + +// Start begins a job that periodically: +// 1) Emits metrics about app version and git commit. +// Note: the metrics daemon is such a simple go-routine that we don't bother implementing a health-check +// for this service. The task loop does not produce any errors because the telemetry calls themselves are +// not error-returning, so in effect this daemon would never become unhealthy. +func Start( + ctx context.Context, + logger log.Logger, +) { + ticker := time.NewTicker(time.Duration(METRICS_DAEMON_LOOP_DELAY_DURATION * time.Millisecond)) + defer ticker.Stop() + for ; true; <-ticker.C { + RunMetricsDaemonTaskLoop( + ctx, + logger, + ) + } +} + +// RunMetricsDaemonTaskLoop contains the logic to emit metrics about the application running. +func RunMetricsDaemonTaskLoop( + ctx context.Context, + logger log.Logger, +) { + // Report out app version and git commit. + version := version.NewInfo() + telemetry.SetGaugeWithLabels( + []string{metrics.AppInfo}, + 1, + []gometrics.Label{ + metrics.GetLabelForStringValue(metrics.AppVersion, version.Version), + metrics.GetLabelForStringValue(metrics.GitCommit, version.GitCommit), + }, + ) + logger.Info( + "App version", + metrics.AppVersion, + version.Version, + metrics.GitCommit, + version.GitCommit, + ) +} diff --git a/daemons/pricefeed/client/client.go b/daemons/pricefeed/client/client.go index a409907af..82dacb5d1 100644 --- a/daemons/pricefeed/client/client.go +++ b/daemons/pricefeed/client/client.go @@ -112,7 +112,7 @@ func (c *Client) Stop() { // A) periodically queries prices from external data sources and saves the retrieved prices in an // in-memory datastore // B) periodically sends the most recent prices to a gRPC server -// C) periodically queries the prices module for the latest market/exchange configuration and then updates +// C) periodically queries the prices module for the latest market/exchange configuration and then updates. !!!!NOTE: this is not implemented!!!! // the shared, in-memory datastore with the latest configuration. // The exchangeIdToQueryConfig map dictates which exchanges the pricefeed client queries against. // For all exchanges included in this map, the pricefeed client expects an exchangeQueryDetails and an @@ -125,7 +125,7 @@ func (c *Client) Stop() { // 3. Initialize synchronized, in-memory shared daemon configuration. // 4. Start PriceEncoder and PriceFetcher per exchange. Each price fetcher adds itself to the shared // daemon config. -// 5. Start MarketUpdater subtask to periodically update the market configs. +// 5. Start MarketUpdater subtask to periodically update the market configs.!!!!NOTE: this is not implemented!!!! THE MARKETPARAMS TYPE IS INJECTED SIMILAR TO // 6. Start PriceUpdater to begin broadcasting prices. func (c *Client) start(ctx context.Context, daemonFlags flags.DaemonFlags, @@ -230,7 +230,7 @@ func (c *Client) start(ctx context.Context, ) }() } - _, _ = priceFeedMutableMarketConfigs.UpdateMarkets(marketParams) + // _, _ = priceFeedMutableMarketConfigs.UpdateMarkets(marketParams) priceUpdaterTicker, priceUpdaterStop := c.newTickerWithStop(int(daemonFlags.Price.LoopDelayMs)) // Now that all persistent subtasks have been started and all tickers and stop channels are created, // signal that the startup process is complete. This needs to be called before entering the diff --git a/daemons/pricefeed/client/client_test.go b/daemons/pricefeed/client/client_test.go new file mode 100644 index 000000000..040c174d2 --- /dev/null +++ b/daemons/pricefeed/client/client_test.go @@ -0,0 +1,918 @@ +package client + +import ( + "context" + "errors" + "fmt" + + "net" + "sort" + "sync" + "testing" + "time" + + appflags "github.com/tellor-io/layer/app/flags" + pricefeed_constants "github.com/tellor-io/layer/daemons/constants" + daemonflags "github.com/tellor-io/layer/daemons/flags" + "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" + handler "github.com/tellor-io/layer/daemons/pricefeed/client/queryhandler" + + daemonserver "github.com/tellor-io/layer/daemons/server" + pricefeed_types "github.com/tellor-io/layer/daemons/server/types/pricefeed" + daemontypes "github.com/tellor-io/layer/daemons/types" + "github.com/tellor-io/layer/testutil/appoptions" + daemontestutils "github.com/tellor-io/layer/testutil/daemons" + grpc_util "github.com/tellor-io/layer/testutil/grpc" + + servertypes "github.com/tellor-io/layer/daemons/server/types" + "google.golang.org/grpc" + + "github.com/cometbft/cometbft/libs/log" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/mocks" + + "github.com/tellor-io/layer/testutil/client" + "github.com/tellor-io/layer/testutil/constants" +) + +var ( + subTaskRunnerImpl = SubTaskRunnerImpl{} +) + +// FakeSubTaskRunner acts as a dummy struct replacing `SubTaskRunner` that simply advances the +// counter for each task in a threadsafe manner and allows awaiting go-routine completion. This +// struct should only be used for testing. +type FakeSubTaskRunner struct { + sync.WaitGroup + sync.Mutex + UpdaterCallCount int + EncoderCallCount int + FetcherCallCount int + MarketUpdaterCallCount int +} + +// StartPriceUpdater replaces `client.StartPriceUpdater` and advances `UpdaterCallCount` by one. +func (f *FakeSubTaskRunner) StartPriceUpdater( + c *Client, + ctx context.Context, + ticker *time.Ticker, + stop <-chan bool, + exchangeToMarketPrices types.ExchangeToMarketPrices, + priceFeedServiceClient servertypes.PriceFeedServiceClient, + logger log.Logger, +) { + // No need to lock/unlock since there is only one updater running and no risk of race-condition. + f.UpdaterCallCount += 1 +} + +// StartPriceEncoder replaces `client.StartPriceEncoder`, marks the embedded waitgroup done and +// advances `EncoderCallCount` by one. This function will be called from a go-routine and is +// threadsafe. +func (f *FakeSubTaskRunner) StartPriceEncoder( + exchangeId types.ExchangeId, + configs types.PricefeedMutableMarketConfigs, + exchangeToMarketPrices types.ExchangeToMarketPrices, + logger log.Logger, + bCh <-chan *price_fetcher.PriceFetcherSubtaskResponse, +) { + f.Lock() + defer f.Unlock() + + f.EncoderCallCount += 1 + f.Done() +} + +// StartPriceFetcher replaces `client.StartPriceFetcher`, marks the embedded waitgroup done and +// advances `FetcherCallCount` by one. This function will be called from a go-routine and is +// threadsafe. +func (f *FakeSubTaskRunner) StartPriceFetcher( + ticker *time.Ticker, + stop <-chan bool, + configs types.PricefeedMutableMarketConfigs, + exchangeQueryConfig types.ExchangeQueryConfig, + exchangeDetails types.ExchangeQueryDetails, + queryHandler handler.ExchangeQueryHandler, + logger log.Logger, + bCh chan<- *price_fetcher.PriceFetcherSubtaskResponse, +) { + f.Lock() + defer f.Unlock() + + f.FetcherCallCount += 1 + f.Done() +} + +const ( + maxBufferedChannelLength = 2 + connectionFailsErrorMsg = "Failed to create connection" + closeConnectionFailsErrorMsg = "Failed to close connection" + fiveKilobytes = 5 * 1024 +) + +var ( + validExchangeId = constants.ExchangeId1 + closeConnectionFailsError = errors.New(closeConnectionFailsErrorMsg) + testExchangeQueryConfigLength = len(constants.TestExchangeQueryConfigs) +) + +func TestFixedBufferSize(t *testing.T) { + require.Equal(t, fiveKilobytes, pricefeed_constants.FixedBufferSize) +} + +func TestStart_InvalidConfig(t *testing.T) { + tests := map[string]struct { + // parameters + mockGrpcClient *mocks.GrpcClient + marketParam []types.MarketParam + initialMarketConfig map[types.MarketId]*types.MutableMarketConfig + initialExchangeMarketConfig map[types.ExchangeId]*types.MutableExchangeMarketConfig + exchangeIdToQueryConfig map[types.ExchangeId]*types.ExchangeQueryConfig + exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails + + // expectations + expectedError error + expectGrpcConnection bool + expectCloseTcpConnection bool + expectCloseGrpcConnection bool + // This should equal the length of the `exchangeIdToQueryConfig` passed into + // `client.Start`. + expectedNumExchangeTasks int + }{ + "Invalid: Tcp Connection Fails": { + mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalTcpConnectionErrors( + errors.New(connectionFailsErrorMsg), + nil, + false, + ), + expectedError: errors.New(connectionFailsErrorMsg), + }, + "Invalid: Grpc Connection Fails": { + mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors( + errors.New(connectionFailsErrorMsg), + nil, + false, + ), + expectedError: errors.New(connectionFailsErrorMsg), + expectGrpcConnection: true, + expectCloseTcpConnection: true, + }, + "Valid: 2 exchanges": { + mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(nil, nil, true), + exchangeIdToQueryConfig: constants.TestExchangeQueryConfigs, + exchangeIdToExchangeDetails: constants.TestExchangeIdToExchangeQueryDetails, + expectGrpcConnection: true, + expectCloseTcpConnection: true, + expectCloseGrpcConnection: true, + expectedNumExchangeTasks: testExchangeQueryConfigLength, + }, + // "Invalid: empty exchange query config": { + // mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(nil, nil, true), + // exchangeIdToQueryConfig: map[types.ExchangeId]*types.ExchangeQueryConfig{}, + // expectedError: errors.New("exchangeIds must not be empty"), + // expectGrpcConnection: true, + // expectCloseTcpConnection: true, + // expectCloseGrpcConnection: true, + // }, + // "Invalid: missing exchange query details": { + // mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(nil, nil, true), + // exchangeIdToQueryConfig: map[string]*types.ExchangeQueryConfig{ + // validExchangeId: constants.TestExchangeQueryConfigs[validExchangeId], + // }, + // expectedError: fmt.Errorf("no exchange details exists for exchangeId: %v", validExchangeId), + // expectGrpcConnection: true, + // expectCloseTcpConnection: true, + // expectCloseGrpcConnection: true, + // }, + // "Invalid: tcp close connection fails with good inputs": { + // mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalTcpConnectionErrors( + // nil, + // closeConnectionFailsError, + // true, + // ), + // exchangeIdToQueryConfig: constants.TestExchangeQueryConfigs, + // exchangeIdToExchangeDetails: constants.TestExchangeIdToExchangeQueryDetails, + // expectedError: closeConnectionFailsError, + // expectGrpcConnection: true, + // expectCloseTcpConnection: true, + // expectCloseGrpcConnection: true, + // expectedNumExchangeTasks: testExchangeQueryConfigLength, + // }, + // "Invalid: grpc close connection fails with good inputs": { + // mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors( + // nil, + // closeConnectionFailsError, + // true, + // ), + // exchangeIdToQueryConfig: constants.TestExchangeQueryConfigs, + // exchangeIdToExchangeDetails: constants.TestExchangeIdToExchangeQueryDetails, + // expectedError: closeConnectionFailsError, + // expectGrpcConnection: true, + // expectCloseTcpConnection: true, + // expectCloseGrpcConnection: true, + // expectedNumExchangeTasks: testExchangeQueryConfigLength, + // }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + faketaskRunner := FakeSubTaskRunner{ + UpdaterCallCount: 0, + EncoderCallCount: 0, + FetcherCallCount: 0, + MarketUpdaterCallCount: 0, + } + + // Wait for each encoder and fetcher call to complete. + faketaskRunner.WaitGroup.Add(tc.expectedNumExchangeTasks * 2) + + // Run Start. + client := newClient(log.NewNopLogger()) + err := client.start( + grpc_util.Ctx, + daemonflags.GetDefaultDaemonFlags(), + appflags.GetFlagValuesFromOptions(appoptions.GetDefaultTestAppOptions("", nil)), + tc.mockGrpcClient, + tc.marketParam, + tc.exchangeIdToQueryConfig, + tc.exchangeIdToExchangeDetails, + &faketaskRunner, + ) + + // Expect daemon is not healthy on startup. Daemon becomes healthy after the first successful market + // update. + require.ErrorContains( + t, + client.HealthCheck(), + "no successful update has occurred", + ) + + if tc.expectedError == nil { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedError.Error()) + } + + // Wait for encoder and fetcher go-routines to complete and then verify each subtask was + // called the expected amount. + faketaskRunner.Wait() + require.Equal(t, tc.expectedNumExchangeTasks, faketaskRunner.EncoderCallCount) + require.Equal(t, tc.expectedNumExchangeTasks, faketaskRunner.FetcherCallCount) + if tc.expectedNumExchangeTasks > 0 { + require.Equal(t, 1, faketaskRunner.UpdaterCallCount) + } else { + require.Equal(t, 0, faketaskRunner.UpdaterCallCount) + } + + tc.mockGrpcClient.AssertCalled(t, "NewTcpConnection", grpc_util.Ctx, grpc_util.TcpEndpoint) + if tc.expectGrpcConnection { + tc.mockGrpcClient.AssertCalled(t, "NewGrpcConnection", grpc_util.Ctx, grpc_util.SocketPath) + } else { + tc.mockGrpcClient.AssertNotCalled(t, "NewGrpcConnection", grpc_util.Ctx, grpc_util.SocketPath) + } + + if tc.expectCloseGrpcConnection { + tc.mockGrpcClient.AssertCalled(t, "CloseConnection", grpc_util.GrpcConn) + } else { + tc.mockGrpcClient.AssertNotCalled(t, "CloseConnection", grpc_util.GrpcConn) + } + + if tc.expectCloseTcpConnection { + tc.mockGrpcClient.AssertCalled(t, "CloseConnection", grpc_util.TcpConn) + } else { + tc.mockGrpcClient.AssertNotCalled(t, "CloseConnection", grpc_util.TcpConn) + } + }) + } +} + +// TestStop tests that the Stop interface works as expected. It's difficult to ensure that each go-routine +// is stopped, but this test ensures that the Stop executes successfully with no hangs. +func TestStop(t *testing.T) { + // Setup daemon and grpc servers. + daemonFlags := daemonflags.GetDefaultDaemonFlags() + appFlags := appflags.GetFlagValuesFromOptions(appoptions.GetDefaultTestAppOptions("", nil)) + + // Configure and run daemon server. + daemonServer := daemonserver.NewServer( + log.NewNopLogger(), + grpc.NewServer(), + &daemontypes.FileHandlerImpl{}, + daemonFlags.Shared.SocketAddress, + ) + daemonServer.WithPriceFeedMarketToExchangePrices( + pricefeed_types.NewMarketToExchangePrices(5 * time.Second), + ) + + defer daemonServer.Stop() + go daemonServer.Start() + + // Create a gRPC server running on the default port and attach the mock prices query response. + grpcServer := grpc.NewServer() + // pricetypes.RegisterQueryServer(grpcServer, &pricesQueryServer) + + // Start gRPC server with cleanup. + defer grpcServer.Stop() + go func() { + ls, err := net.Listen("tcp", appFlags.GrpcAddress) + require.NoError(t, err) + err = grpcServer.Serve(ls) + require.NoError(t, err) + }() + + client := StartNewClient( + grpc_util.Ctx, + daemonFlags, + appFlags, + log.NewNopLogger(), + &daemontypes.GrpcClientImpl{}, + []types.MarketParam{}, + constants.TestExchangeQueryConfigs, + constants.TestExchangeIdToExchangeQueryDetails, + &SubTaskRunnerImpl{}, + ) + + // Stop the daemon. + client.Stop() +} + +func TestPriceEncoder_NoWrites(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + runPriceEncoderSequentially( + t, + constants.ExchangeId1, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1), + etmp, + bChMap[constants.ExchangeId1], + []*types.MarketPriceTimestamp{}, + ) + + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp) + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp) + require.Empty(t, bChMap[constants.ExchangeId1]) + require.Empty(t, bChMap[constants.ExchangeId2]) +} + +func TestPriceEncoder_DoNotWriteError(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + bCh := bChMap[constants.ExchangeId1] + bCh <- &price_fetcher.PriceFetcherSubtaskResponse{ + Price: nil, + Err: errors.New("Failed to query"), + } + close(bCh) + + configs := genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1) + + subTaskRunnerImpl.StartPriceEncoder(constants.ExchangeId1, configs, etmp, log.NewNopLogger(), bCh) + + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp) + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp) + require.Empty(t, bChMap[constants.ExchangeId1]) + require.Empty(t, bChMap[constants.ExchangeId2]) +} + +func TestPriceEncoder_WriteToOneMarket(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + runPriceEncoderSequentially( + t, + constants.ExchangeId1, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1), + etmp, + bChMap[constants.ExchangeId1], + []*types.MarketPriceTimestamp{ + constants.Market9_TimeT_Price1, + }, + ) + + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp, 1) + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp) + + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price1, + LastUpdateTime: constants.TimeT, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId9], + ) +} + +func TestPriceEncoder_WriteToTwoMarkets(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + runPriceEncoderSequentially( + t, + constants.ExchangeId1, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1), + etmp, + bChMap[constants.ExchangeId1], + []*types.MarketPriceTimestamp{ + constants.Market9_TimeT_Price1, + constants.Market8_TimeTMinusThreshold_Price2, + }, + ) + + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp, 2) + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp) + + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price1, + LastUpdateTime: constants.TimeT, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId9], + ) + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price2, + LastUpdateTime: constants.TimeTMinusThreshold, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId8], + ) +} + +func TestPriceEncoder_WriteToOneMarketTwice(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + runPriceEncoderSequentially( + t, + constants.ExchangeId1, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1), + etmp, + bChMap[constants.ExchangeId1], + []*types.MarketPriceTimestamp{ + constants.Market9_TimeTMinusThreshold_Price2, + constants.Market9_TimeT_Price1, + }, + ) + + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp, 1) + require.Empty(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp) + + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price1, + LastUpdateTime: constants.TimeT, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId9], + ) +} + +func TestPriceEncoder_WriteToTwoExchanges(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + runPriceEncoderSequentially( + t, + constants.ExchangeId1, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1), + etmp, + bChMap[constants.ExchangeId1], + []*types.MarketPriceTimestamp{ + constants.Market9_TimeT_Price1, + }, + ) + + runPriceEncoderSequentially( + t, + constants.ExchangeId2, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId2), + etmp, + bChMap[constants.ExchangeId2], + []*types.MarketPriceTimestamp{ + constants.Market8_TimeTMinusThreshold_Price2, + }, + ) + + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp, 1) + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp, 1) + + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price1, + LastUpdateTime: constants.TimeT, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId9], + ) + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price2, + LastUpdateTime: constants.TimeTMinusThreshold, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp[constants.MarketId8], + ) +} + +func TestPriceEncoder_WriteToTwoExchangesConcurrentlyWithManyUpdates(t *testing.T) { + etmp, bChMap := generateBufferedChannelAndExchangeToMarketPrices(t, constants.Exchange1Exchange2Array) + + largeMarketWrite := []*types.MarketPriceTimestamp{ + constants.Market8_TimeTMinusThreshold_Price1, + constants.Market8_TimeTMinusThreshold_Price2, + constants.Market8_TimeTMinusThreshold_Price3, + constants.Market9_TimeTMinusThreshold_Price1, + constants.Market9_TimeTMinusThreshold_Price2, + constants.Market9_TimeTMinusThreshold_Price3, + constants.Market8_TimeT_Price3, + constants.Market9_TimeT_Price1, + constants.Market9_TimeT_Price2, + constants.Market9_TimeT_Price3, + constants.Market9_TimeTPlusThreshold_Price1, + } + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + runPriceEncoderConcurrently( + constants.ExchangeId1, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId1), + etmp, + bChMap[constants.ExchangeId1], + largeMarketWrite, + ) + }() + + wg.Add(1) + go func() { + defer wg.Done() + runPriceEncoderConcurrently( + constants.ExchangeId2, + genMockPricefeedMutableMarketConfigsForExchange(constants.ExchangeId2), + etmp, + bChMap[constants.ExchangeId2], + largeMarketWrite, + ) + }() + + wg.Wait() + + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp, 2) + require.Len(t, etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp, 2) + + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price1, + LastUpdateTime: constants.TimeTPlusThreshold, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId9], + ) + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price3, + LastUpdateTime: constants.TimeT, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId1].MarketToPriceTimestamp[constants.MarketId8], + ) + + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price1, + LastUpdateTime: constants.TimeTPlusThreshold, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp[constants.MarketId9], + ) + require.Equal( + t, + &pricefeedtypes.PriceTimestamp{ + Price: constants.Price3, + LastUpdateTime: constants.TimeT, + }, + etmp.ExchangeMarketPrices[constants.ExchangeId2].MarketToPriceTimestamp[constants.MarketId8], + ) +} + +func TestPriceUpdater_Mixed(t *testing.T) { + tests := map[string]struct { + // parameters + exchangeAndMarketPrices []*client.ExchangeIdMarketPriceTimestamp + priceUpdateError error + + // expectations + expectedMarketPriceUpdate []*servertypes.MarketPriceUpdate + }{ + "Update throws": { + // Throws error due to mock so that we can simulate fail state. + exchangeAndMarketPrices: []*client.ExchangeIdMarketPriceTimestamp{ + constants.ExchangeId1_Market9_TimeT_Price1, + }, + priceUpdateError: errors.New("failed to send price update"), + }, + "No exchange market prices, does not call `UpdateMarketPrices`": { + exchangeAndMarketPrices: []*client.ExchangeIdMarketPriceTimestamp{}, + priceUpdateError: fmt.Errorf("ErrEmptyMarketPriceUpdate"), + }, + "One market for one exchange": { + exchangeAndMarketPrices: []*client.ExchangeIdMarketPriceTimestamp{ + constants.ExchangeId1_Market9_TimeT_Price1, + }, + expectedMarketPriceUpdate: constants.Market9_SingleExchange_AtTimeUpdate, + }, + "Three markets at timeT": { + exchangeAndMarketPrices: []*client.ExchangeIdMarketPriceTimestamp{ + constants.ExchangeId1_Market9_TimeT_Price1, + constants.ExchangeId2_Market9_TimeT_Price2, + constants.ExchangeId2_Market8_TimeT_Price2, + constants.ExchangeId3_Market8_TimeT_Price3, + constants.ExchangeId1_Market7_TimeT_Price1, + constants.ExchangeId3_Market7_TimeT_Price3, + }, + expectedMarketPriceUpdate: constants.AtTimeTPriceUpdate, + }, + "Three markets at mixed time": { + exchangeAndMarketPrices: []*client.ExchangeIdMarketPriceTimestamp{ + constants.ExchangeId1_Market9_TimeT_Price1, + constants.ExchangeId2_Market9_TimeT_Price2, + constants.ExchangeId3_Market9_TimeT_Price3, + constants.ExchangeId1_Market8_BeforeTimeT_Price3, + constants.ExchangeId2_Market8_TimeT_Price2, + constants.ExchangeId3_Market8_TimeT_Price3, + constants.ExchangeId2_Market7_BeforeTimeT_Price1, + constants.ExchangeId1_Market7_BeforeTimeT_Price3, + constants.ExchangeId3_Market7_TimeT_Price3, + }, + expectedMarketPriceUpdate: constants.MixedTimePriceUpdate, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Create `ExchangeIdMarketPriceTimestamp` and populate it with market-price updates. + etmp, _ := types.NewExchangeToMarketPrices( + []types.ExchangeId{ + constants.ExchangeId1, + constants.ExchangeId2, + constants.ExchangeId3, + }, + ) + for _, exchangeAndMarketPrice := range tc.exchangeAndMarketPrices { + etmp.UpdatePrice( + exchangeAndMarketPrice.ExchangeId, + exchangeAndMarketPrice.MarketPriceTimestamp, + ) + } + + // Create a mock `PriceFeedServiceClient` and run `RunPriceUpdaterTaskLoop`. + mockPriceFeedClient := generateMockQueryClient() + mockPriceFeedClient.On("UpdateMarketPrices", grpc_util.Ctx, mock.Anything). + Return(nil, tc.priceUpdateError) + + err := RunPriceUpdaterTaskLoop( + grpc_util.Ctx, + etmp, + mockPriceFeedClient, + log.NewNopLogger(), + ) + require.Equal( + t, + tc.priceUpdateError, + err, + ) + + // We sort the `expectedUpdates` as ordering is not guaranteed. + // We then verify `UpdateMarketPrices` was called with an update that, when sorted, matches + // the sorted `expectedUpdates`. + expectedUpdates := tc.expectedMarketPriceUpdate + sortMarketPriceUpdateByMarketIdDescending(expectedUpdates) + + if tc.expectedMarketPriceUpdate != nil { + mockPriceFeedClient.AssertCalled( + t, + "UpdateMarketPrices", + grpc_util.Ctx, + mock.MatchedBy(func(i interface{}) bool { + param := i.(*servertypes.UpdateMarketPricesRequest) + updates := param.MarketPriceUpdates + sortMarketPriceUpdateByMarketIdDescending(updates) + + for i, update := range updates { + prices := update.ExchangePrices + require.ElementsMatch( + t, + expectedUpdates[i].ExchangePrices, + prices, + ) + } + return true + }), + ) + } else { + mockPriceFeedClient.AssertNotCalled(t, "UpdateMarketPrices") + } + }) + } +} + +func TestHealthCheck_Mixed(t *testing.T) { + tests := map[string]struct { + updateMarketPricesError error + expectedError error + }{ + "No error - daemon healthy": { + updateMarketPricesError: nil, + expectedError: nil, + }, + "Error - daemon unhealthy": { + updateMarketPricesError: fmt.Errorf("failed to update market prices"), + expectedError: fmt.Errorf( + "failed to run price updater task loop for price daemon; failed to update market prices", + ), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup. + // Create `ExchangeIdMarketPriceTimestamp` and populate it with market-price updates. + etmp, err := types.NewExchangeToMarketPrices([]types.ExchangeId{constants.ExchangeId1}) + require.NoError(t, err) + etmp.UpdatePrice(constants.ExchangeId1, constants.Market9_TimeT_Price1) + + // Create a mock `PriceFeedServiceClient`. + mockPriceFeedClient := generateMockQueryClient() + + // Mock the `UpdateMarketPrices` call to return an error if specified. + mockPriceFeedClient.On("UpdateMarketPrices", grpc_util.Ctx, mock.Anything). + Return(nil, tc.updateMarketPricesError).Once() + + ticker, stop := daemontestutils.SingleTickTickerAndStop() + client := newClient(log.NewNopLogger()) + + // Act. + // Run the price updater for a single tick. Expect the daemon to toggle health state based on + // `UpdateMarketPrices` error response. + subTaskRunnerImpl.StartPriceUpdater( + client, + grpc_util.Ctx, + ticker, + stop, + etmp, + mockPriceFeedClient, + log.NewNopLogger(), + ) + + // Assert. + if tc.expectedError == nil { + require.NoError(t, client.HealthCheck()) + } else { + require.ErrorContains(t, client.HealthCheck(), tc.expectedError.Error()) + } + }) + } +} + +// // ----------------- Generate Mock Instances ----------------- // + +// // generateMockQueryClient generates a mock QueryClient that can be used to support any of the QueryClient +// // interfaces added to the mocks.QueryClient class, including the prices query client and the +// // pricefeed service client. +func generateMockQueryClient() *mocks.QueryClient { + mockPriceFeedServiceClient := &mocks.QueryClient{} + + return mockPriceFeedServiceClient +} + +// ----------------- Helper Functions ----------------- // + +func generateBufferedChannelAndExchangeToMarketPrices( + t *testing.T, + exchangeIds []types.ExchangeId, +) ( + *types.ExchangeToMarketPricesImpl, + map[types.ExchangeId]chan *price_fetcher.PriceFetcherSubtaskResponse, +) { + _etmp, err := types.NewExchangeToMarketPrices(exchangeIds) + etmp := _etmp.(*types.ExchangeToMarketPricesImpl) + require.NoError(t, err) + require.NotNil(t, etmp) + + exchangeIdToBufferedChannel := + map[types.ExchangeId]chan *price_fetcher.PriceFetcherSubtaskResponse{} + for _, exchangeId := range exchangeIds { + bCh := make(chan *price_fetcher.PriceFetcherSubtaskResponse, maxBufferedChannelLength) + exchangeIdToBufferedChannel[exchangeId] = bCh + } + + return etmp, exchangeIdToBufferedChannel +} + +func runPriceEncoderSequentially( + t *testing.T, + exchangeId types.ExchangeId, + configs types.PricefeedMutableMarketConfigs, + etmp types.ExchangeToMarketPrices, + bCh chan *price_fetcher.PriceFetcherSubtaskResponse, + writes []*types.MarketPriceTimestamp, +) { + // Make sure there are not more write than the `bufferedChannel` can hold. + require.True(t, len(writes) <= maxBufferedChannelLength) + + for _, write := range writes { + bCh <- &price_fetcher.PriceFetcherSubtaskResponse{ + Price: write, + Err: nil, + } + } + + close(bCh) + subTaskRunnerImpl.StartPriceEncoder(exchangeId, configs, etmp, log.NewNopLogger(), bCh) +} + +func runPriceEncoderConcurrently( + exchangeId types.ExchangeId, + configs types.PricefeedMutableMarketConfigs, + etmp types.ExchangeToMarketPrices, + bCh chan *price_fetcher.PriceFetcherSubtaskResponse, + writes []*types.MarketPriceTimestamp, +) { + // Start a `waitGroup` for the `PriceEncoder` which will complete when the `bufferedChannel` + // is empty and is closed. + var priceEncoderWg sync.WaitGroup + priceEncoderWg.Add(1) + go func() { + defer priceEncoderWg.Done() + subTaskRunnerImpl.StartPriceEncoder(exchangeId, configs, etmp, log.NewNopLogger(), bCh) + }() + + // Start a `waitGroup` for threads that will write to the `bufferedChannel`. + var writeWg sync.WaitGroup + for _, write := range writes { + writeWg.Add(1) + go func(write *types.MarketPriceTimestamp) { + defer writeWg.Done() + bCh <- &price_fetcher.PriceFetcherSubtaskResponse{ + Price: write, + Err: nil, + } + }(write) + } + + writeWg.Wait() + close(bCh) + priceEncoderWg.Wait() +} + +func sortMarketPriceUpdateByMarketIdDescending( + marketPriceUpdate []*servertypes.MarketPriceUpdate, +) { + sort.Slice( + marketPriceUpdate, + func(i, j int) bool { + return marketPriceUpdate[i].MarketId > marketPriceUpdate[j].MarketId + }, + ) +} + +func genMockPricefeedMutableMarketConfigsForExchange( + exchangeId types.ExchangeId, +) types.PricefeedMutableMarketConfigs { + mutableExchangeConfig := &types.MutableExchangeMarketConfig{ + Id: exchangeId, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 8: { + Ticker: "MARKET8-USD", + }, + 9: { + Ticker: "MARKET9-USD", + }, + }, + } + mutableMarketConfigs := []*types.MutableMarketConfig{ + { + Id: constants.MarketId8, + Pair: "MARKET8-USD", + Exponent: -9, + MinExchanges: 1, + }, + { + Id: constants.MarketId9, + Pair: "MARKET9-USD", + Exponent: -9, + MinExchanges: 1, + }, + } + configs := &mocks.PricefeedMutableMarketConfigs{} + configs.On("GetExchangeMarketConfigCopy", exchangeId).Return(mutableExchangeConfig, nil) + + // All possible permutations of supported markets. + configs.On("GetMarketConfigCopies", []types.MarketId{8, 9}).Return(mutableMarketConfigs, nil) + configs.On("GetMarketConfigCopies", []types.MarketId{8}).Return(mutableMarketConfigs[0:1], nil) + configs.On("GetMarketConfigCopies", []types.MarketId{9}).Return(mutableMarketConfigs[1:2], nil) + configs.On("GetMarketConfigCopies", []types.MarketId{}).Return([]*types.MutableMarketConfig{}, nil) + + configs.On("AddPriceFetcher", mock.Anything).Return(nil) + configs.On("AddPriceEncoder", mock.Anything).Return(nil) + return configs +} diff --git a/daemons/pricefeed/client/price_encoder/mutable_state_test.go b/daemons/pricefeed/client/price_encoder/mutable_state_test.go new file mode 100644 index 000000000..c495e5ce2 --- /dev/null +++ b/daemons/pricefeed/client/price_encoder/mutable_state_test.go @@ -0,0 +1,132 @@ +package price_encoder + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +func newMarketIdWithValue(id types.MarketId) *types.MarketId { + ptr := new(types.MarketId) + *ptr = id + return ptr +} + +func TestGetPriceConversionDetailsForMarket(t *testing.T) { + tests := map[string]struct { + mutableExchangeConfig *types.MutableExchangeMarketConfig + marketToMutableConfig map[types.MarketId]*types.MutableMarketConfig + expected priceConversionDetails + expectedErr error + }{ + "Error: Market config not found": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: "exchange1", + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{}, + }, + marketToMutableConfig: map[types.MarketId]*types.MutableMarketConfig{}, + expectedErr: fmt.Errorf("market config for market 1 not found on exchange 'exchange1'"), + }, + "Error: Mutable market config for adjustment market not found": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: "exchange1", + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + AdjustByMarket: newMarketIdWithValue(2), + }, + }, + }, + marketToMutableConfig: map[types.MarketId]*types.MutableMarketConfig{}, + expectedErr: fmt.Errorf( + "mutable market config for adjust-by market 2 not found on exchange 'exchange1'", + ), + }, + "Error: Mutable market config for market not found": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: "exchange1", + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + }, + }, + }, + marketToMutableConfig: map[types.MarketId]*types.MutableMarketConfig{}, + expectedErr: fmt.Errorf("mutable market config for market 1 not found on exchange 'exchange1'"), + }, + "Success: no adjustment market": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: "exchange1", + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + }, + }, + }, + marketToMutableConfig: map[types.MarketId]*types.MutableMarketConfig{ + 1: { + Id: 1, + Pair: "PAIR-USD", + Exponent: -5, + MinExchanges: 2, + }, + }, + expected: priceConversionDetails{ + Exponent: -5, + }, + }, + "Success: with adjustment market": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: "exchange1", + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + AdjustByMarket: newMarketIdWithValue(2), + Invert: true, + }, + }, + }, + marketToMutableConfig: map[types.MarketId]*types.MutableMarketConfig{ + 1: { + Id: 1, + Pair: "PAIR-USD", + Exponent: -5, + MinExchanges: 2, + }, + 2: { + Id: 2, + Pair: "ADJ-USD", + Exponent: -6, + MinExchanges: 3, + }, + }, + expected: priceConversionDetails{ + Invert: true, + Exponent: -5, + AdjustByMarketDetails: &adjustByMarketDetails{ + MarketId: 2, + Exponent: -6, + MinExchanges: 3, + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + mutableState := &mutableState{ + mutableExchangeConfig: tc.mutableExchangeConfig, + marketToMutableConfig: tc.marketToMutableConfig, + } + actual, actualErr := mutableState.GetPriceConversionDetailsForMarket(1) + if tc.expectedErr != nil { + require.Error(t, tc.expectedErr, actualErr) + require.Zero(t, actual) + } else { + require.NoError(t, actualErr) + require.Equal(t, tc.expected, actual) + } + }) + } +} diff --git a/daemons/pricefeed/client/price_encoder/price_encoder_test.go b/daemons/pricefeed/client/price_encoder/price_encoder_test.go new file mode 100644 index 000000000..b210b5870 --- /dev/null +++ b/daemons/pricefeed/client/price_encoder/price_encoder_test.go @@ -0,0 +1,420 @@ +package price_encoder + +import ( + "context" + "errors" + "fmt" + "syscall" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/log" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + pf_constants "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pft "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/lib/metrics" + "github.com/tellor-io/layer/mocks" + "github.com/tellor-io/layer/testutil/constants" +) + +const ( + FailedToUpdateExchangePriceMsg = "Failed to update exchange price in price daemon priceEncoder" + GenericExchangeErrorMsg = "http2: client connection force closed via ClientConn.Close" +) + +func generateBufferedChannelAndExchangeToMarketPrices( + t *testing.T, + exchangeIds []types.ExchangeId, +) ( + types.ExchangeToMarketPrices, + chan *price_fetcher.PriceFetcherSubtaskResponse, +) { + etmp, err := types.NewExchangeToMarketPrices(exchangeIds) + require.NoError(t, err) + + bCh := make(chan *price_fetcher.PriceFetcherSubtaskResponse, pf_constants.FixedBufferSize) + + return etmp.(*types.ExchangeToMarketPricesImpl), bCh +} + +func genNewPriceEncoder(t *testing.T) *PriceEncoderImpl { + etmp, bCh := generateBufferedChannelAndExchangeToMarketPrices(t, []types.ExchangeId{constants.ExchangeId1}) + pe, err := NewPriceEncoder( + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + etmp, + log.TestingLogger(), + bCh, + ) + require.NoError(t, err) + return pe +} + +func TestGetExchangeId(t *testing.T) { + // 1. Setup + pe := genNewPriceEncoder(t) + + // 2. Test + require.Equal(t, constants.ExchangeId1, pe.GetExchangeId()) +} + +func TestUpdateMutableExchangeConfig_Mixed(t *testing.T) { + tests := map[string]struct { + updateExchangeConfig *types.MutableExchangeMarketConfig + updateMarketConfigs []*types.MutableMarketConfig + expectedError error + }{ + "Failed - Exchange ID mismatch": { + updateExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: constants.ExchangeId2, + }, + updateMarketConfigs: constants.MutableMarketConfigs_3Markets, + expectedError: fmt.Errorf( + "PriceEncoder.UpdateMutableExchangeConfig: exchange id mismatch, expected '%v', got '%v'", + constants.ExchangeId1, + constants.ExchangeId2, + ), + }, + "Failed - Invalid config": { + updateExchangeConfig: &constants.Exchange1_1Markets_MutableExchangeMarketConfig, + updateMarketConfigs: []*types.MutableMarketConfig{}, + expectedError: fmt.Errorf( + "PriceEncoder.UpdateMutableExchangeConfig: invalid exchange config update: no market config " + + "for market 7 on exchange 'Exchange1'"), + }, + "Success": { + updateExchangeConfig: &constants.Exchange1_5Markets_MutableExchangeMarketConfig, + updateMarketConfigs: constants.MutableMarketConfigs_5Markets, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + pe := genNewPriceEncoder(t) + err := pe.UpdateMutableExchangeConfig(tc.updateExchangeConfig, tc.updateMarketConfigs) + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +type MockExchangeToMarketPrices struct { + types.ExchangeToMarketPrices + indexPrice uint64 + numPricesMedianized int +} + +func (m *MockExchangeToMarketPrices) GetIndexPrice(types.MarketId, time.Time, pft.Resolver) (uint64, int) { + return m.indexPrice, m.numPricesMedianized +} + +func TestConvertPriceUpdate_Mixed(t *testing.T) { + tests := map[string]struct { + mutableExchangeConfig *types.MutableExchangeMarketConfig + mutableMarketConfigs []*types.MutableMarketConfig + adjustmentMarketIndexPrice uint64 + adjustmentMarketNumPricesMedianized int + expectedPrice uint64 + expectedErr error + }{ + "Success - no conversion": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: constants.ExchangeId1, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + }, + }, + }, + mutableMarketConfigs: []*types.MutableMarketConfig{ + { + Id: 1, + Pair: "PAIR-USD", + Exponent: -6, + MinExchanges: 1, + }, + }, + expectedPrice: constants.FiveBillion, + }, + "Success - inverted price": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: constants.ExchangeId1, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + Invert: true, + }, + }, + }, + mutableMarketConfigs: []*types.MutableMarketConfig{ + { + Id: 1, + Pair: "PAIR-USD", + Exponent: -10, + MinExchanges: 1, + }, + }, + expectedPrice: uint64(20_000_000_000), + }, + "Success - division with adjust-by market": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: constants.ExchangeId1, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + AdjustByMarket: newMarketIdWithValue(2), + Invert: true, + }, + }, + }, + mutableMarketConfigs: []*types.MutableMarketConfig{ + { + Id: 1, + Pair: "PAIR-USD", + Exponent: -6, + MinExchanges: 1, + }, + { + Id: 2, + Pair: "ADJ-USD", + Exponent: -10, + MinExchanges: 1, + }, + }, + adjustmentMarketIndexPrice: constants.FiveBillion * 15_000, // 1.5x price. + adjustmentMarketNumPricesMedianized: 1, + expectedPrice: uint64(1_500_000), // Expect 1.5e6. + }, + "Success - multiplication with adjust-by market": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: constants.ExchangeId1, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + AdjustByMarket: newMarketIdWithValue(2), + }, + }, + }, + mutableMarketConfigs: []*types.MutableMarketConfig{ + { + Id: 1, + Pair: "PAIR-USD", + Exponent: -6, + MinExchanges: 1, + }, + { + Id: 2, + Pair: "ADJ-USD", + Exponent: -9, + MinExchanges: 1, + }, + }, + adjustmentMarketIndexPrice: uint64(990_000_000), // 0.99. + adjustmentMarketNumPricesMedianized: 1, + expectedPrice: uint64(4_950_000_000), // 5 billion * 99%. + }, + "Failure - invalid index price": { + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: constants.ExchangeId1, + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + 1: { + Ticker: "PAIR-USD", + AdjustByMarket: newMarketIdWithValue(2), + }, + }, + }, + mutableMarketConfigs: []*types.MutableMarketConfig{ + { + Id: 1, + Pair: "PAIR-USD", + Exponent: -6, + MinExchanges: 1, + }, + { + Id: 2, + Pair: "ADJ-USD", + Exponent: -9, + MinExchanges: 2, + }, + }, + adjustmentMarketIndexPrice: uint64(990_000_000), + adjustmentMarketNumPricesMedianized: 1, // Should be at least 2. + expectedErr: fmt.Errorf( + "Could not retrieve index price for market 2: expected median price from 2 exchanges, but got " + + "1 exchanges)", + ), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + emtp := MockExchangeToMarketPrices{ + indexPrice: tc.adjustmentMarketIndexPrice, + numPricesMedianized: tc.adjustmentMarketNumPricesMedianized, + } + pe, err := NewPriceEncoder( + tc.mutableExchangeConfig, + tc.mutableMarketConfigs, + &emtp, + log.TestingLogger(), + nil, + ) + require.NoError(t, err) + convertedPriceTimestamp, err := pe.convertPriceUpdate( + &types.MarketPriceTimestamp{ + MarketId: constants.MarketId1, + Price: constants.FiveBillion, + LastUpdatedAt: constants.TimeT, + }, + ) + if tc.expectedErr != nil { + require.Error(t, tc.expectedErr, err.Error()) + require.Zero(t, convertedPriceTimestamp) + } else { + require.NoError(t, err) + require.Equal(t, constants.TimeT, convertedPriceTimestamp.LastUpdatedAt) + require.Equal(t, constants.MarketId1, convertedPriceTimestamp.MarketId) + require.Equal(t, tc.expectedPrice, convertedPriceTimestamp.Price) + } + }) + } +} + +func TestUpdatePrice_Failure(t *testing.T) { + tests := map[string]struct { + isPastGracePeriod bool + }{ + "Failed - past grace period": { + isPastGracePeriod: true, + }, + "Failed - not past grace period": {}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + logger := &mocks.Logger{} + etmp := &mocks.ExchangeToMarketPrices{} + price_encoder := PriceEncoderImpl{ + isPastGracePeriod: tc.isPastGracePeriod, + logger: logger, + mutableState: &mutableState{ + mutableExchangeConfig: &types.MutableExchangeMarketConfig{ + Id: "Binance", + }, + }, + exchangeId: "Binance", + exchangeToMarketPrices: etmp, + } + + // We expect failures to be logged. If the daemon is past the grace period, the failure will be logged as + // an error. Otherwise, the failure will be logged as info. + logMethod := "Info" + if tc.isPastGracePeriod { + logMethod = "Error" + } + logger.On( + logMethod, + "Failed to get price conversion details for market", + "error", + errors.New("market config for market 0 not found on exchange 'Binance'"), + "marketId", + types.MarketId(0), + "exchangeId", + "Binance", + ).Return() + + // Intentionally send an invalid price update to trigger error cascade. + price_encoder.UpdatePrice(&types.MarketPriceTimestamp{}) + + // Validate that expected log method is called, and exchangeToMarketPrices.UpdatePrices is never called. + mock.AssertExpectationsForObjects(t, logger, etmp) + }) + } +} + +func TestProcessPriceFetcherResponse_Error(t *testing.T) { + tests := map[string]struct { + err error + isUnidentifiedError bool + logAsError bool + expectedReason string + }{ + "Deadline exceeded error": { + err: context.DeadlineExceeded, + expectedReason: metrics.HttpGetTimeout, + }, + "Rate limit error": { + err: pf_constants.RateLimitingError, + logAsError: true, + expectedReason: metrics.RateLimit, + }, + "Exchange-specific error": { + err: sources.NewExchangeError("Binance", "exchange-specific error"), + expectedReason: metrics.ExchangeSpecificError, + }, + "Generic exchange error": { + err: errors.New(GenericExchangeErrorMsg), + expectedReason: metrics.HttpGet5xx, + }, + "Connection reset error": { + err: syscall.ECONNRESET, + expectedReason: metrics.HttpGetHangup, + }, + "Unidentified error": { + err: errors.New("unidentified error"), + isUnidentifiedError: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + logger := &mocks.Logger{} + etmp := &mocks.ExchangeToMarketPrices{} + price_encoder := PriceEncoderImpl{ + logger: logger, + exchangeId: "Binance", + exchangeToMarketPrices: etmp, + } + + // Unidentified errors are logged without reason key-value pairs in the log message. + if tc.isUnidentifiedError { + logger.On( + "Error", + FailedToUpdateExchangePriceMsg, + "error", + tc.err, + "exchangeId", + "Binance", + ).Return().Once() + } else { + logMethod := "Info" + if tc.logAsError { + logMethod = "Error" + } + logger.On( + logMethod, + FailedToUpdateExchangePriceMsg, + "reason", + tc.expectedReason, + "exchangeId", + "Binance", + "error", + tc.err, + ).Return().Once() + } + + price_encoder.ProcessPriceFetcherResponse( + &price_fetcher.PriceFetcherSubtaskResponse{ + Err: tc.err, + }, + ) + + // Validate correct log method is called. Validate exchangeToMarketPrices is never updated. + mock.AssertExpectationsForObjects(t, logger, etmp) + }) + } +} diff --git a/daemons/server/server.go b/daemons/server/server.go index 84552d619..6a25a5e9a 100644 --- a/daemons/server/server.go +++ b/daemons/server/server.go @@ -28,8 +28,6 @@ type Server struct { } // NewServer creates a single gRPC server that's shared across multiple daemons for communication. -// uniqueTestIdentifier is a string that can be used to identify services spawned by a particular test case, -// so that they can be cleaned up after the test case is complete. func NewServer( logger log.Logger, grpcServer daemontypes.GrpcServer, @@ -49,10 +47,6 @@ func (server *Server) Stop() { server.gsrv.Stop() } -func (server *Server) ser() *Server { - return server -} - // reportValidResponse reports a valid request/response from a daemon service for metrics collection purposes. func (server *Server) reportValidResponse(daemonKey string) { telemetry.IncrCounterWithLabels( diff --git a/daemons/server/types/health_checker.go b/daemons/server/types/health_checker.go new file mode 100644 index 000000000..d9bc77729 --- /dev/null +++ b/daemons/server/types/health_checker.go @@ -0,0 +1,228 @@ +package types + +import ( + "sync" + "time" + + "github.com/cometbft/cometbft/libs/log" + "github.com/tellor-io/layer/daemons/types" + libtime "github.com/tellor-io/layer/lib/time" +) + +// errorStreak tracks two relevant statistics for an error streak returned by a HealthCheckable - the timestamp of the +// beginning of the error streak, and the most recent error. This is useful for determining how long a service has been +// unhealthy, as well as the current state of the service. +type errorStreak struct { + startOfStreak time.Time + mostRecentError error +} + +// UpdateLastError updates the errorStreak to reflect the current error. If the startOfStreak timestamp is zero, this +// error the first error in a new error streak, so the startOfStreak timestamp is set to the current timestamp. +func (u *errorStreak) UpdateLastError(timestamp time.Time, err error) { + // If the startOfStreak is zero, this is the first update, so set the startOfStreak. + if u.startOfStreak.IsZero() { + u.startOfStreak = timestamp + } + + u.mostRecentError = err +} + +// Reset resets the errorStreak to its zero value, indicating that the service has no active error streak. +func (u *errorStreak) Reset() { + u.startOfStreak = time.Time{} + u.mostRecentError = nil +} + +// IsUnset returns true if the errorStreak is unset, indicating that the service has no active error streak. +func (u *errorStreak) IsUnset() bool { + return u.startOfStreak.IsZero() && u.mostRecentError == nil +} + +// StartOfStreak returns the timestamp of th start of the most recent error streak. +func (u *errorStreak) StartOfStreak() time.Time { + return u.startOfStreak +} + +// MostRecentError returns the most recent error associated with the current error streak. +func (u *errorStreak) MostRecentError() error { + return u.mostRecentError +} + +// healthCheckerMutableState tracks the current health state of the HealthCheckable, encapsulating all mutable state +// into a single struct for ease of synchronization. +type healthCheckerMutableState struct { + // lock is used to synchronize access to mutable state fields. + lock sync.Mutex + + // lastSuccessTimestamp is the startOfStreak of the most recent successful health check. + // Access to lastSuccessTimestamp is synchronized. + lastSuccessTimestamp time.Time + + // mostRecentErrorStreak tracks the beginning of the most recent streak, as well as the current error in the streak. + // It is updated on every error and reset every time the service sees a healthy response. + // This field is used to determine how long the daemon has been unhealthy. If the mostRecentErrorStreak is unset, + // then either the service has never been unhealthy, or the most recent error streak ended before it could trigger + // a callback. + // Access to mostRecentErrorStreak is synchronized. + mostRecentErrorStreak errorStreak + + // timer triggers a health check poll for a health-checkable service. + timer *time.Timer + + // stopped indicates whether the health checker has been stopped. Additional health checks cannot be scheduled + // after the health checker has been stopped. + stopped bool +} + +// ReportSuccess updates the health checker's mutable state to reflect a successful health check and schedules the next +// poll as an atomic operation. +func (u *healthCheckerMutableState) ReportSuccess(now time.Time) { + u.lock.Lock() + defer u.lock.Unlock() + + u.lastSuccessTimestamp = now + + // Whenever the service is healthy, reset the first failure in streak startOfStreak. + u.mostRecentErrorStreak.Reset() +} + +// ReportFailure updates the health checker's mutable state to reflect a failed health check and schedules the next +// poll as an atomic operation. The method returns the duration of the current failure streak. +func (u *healthCheckerMutableState) ReportFailure(now time.Time, err error) time.Duration { + u.lock.Lock() + defer u.lock.Unlock() + + u.mostRecentErrorStreak.UpdateLastError(now, err) + + return now.Sub(u.mostRecentErrorStreak.StartOfStreak()) +} + +// SchedulePoll schedules the next poll for the health-checkable service. If the service is stopped, the next poll +// will not be scheduled. This method is synchronized. +func (u *healthCheckerMutableState) SchedulePoll(nextPollDelay time.Duration) { + u.lock.Lock() + defer u.lock.Unlock() + + // Don't schedule a poll if the health checker has been stopped. + if u.stopped { + return + } + + // Schedule the next poll. + u.timer.Reset(nextPollDelay) +} + +// InitializePolling schedules the first poll for the health-checkable service. This method is meant to be called +// immediately after initializing the health checker mutable state. This method is synchronized. +func (u *healthCheckerMutableState) InitializePolling(firstPollDelay time.Duration, pollFunc func()) { + u.lock.Lock() + defer u.lock.Unlock() + + // If the timer is already initialized, don't initialize it again. + if u.timer != nil { + return + } + + // The first poll is scheduled after a custom delay to allow the service to initialize. + u.timer = time.AfterFunc(firstPollDelay, pollFunc) +} + +// Stop stops the health checker. This method is synchronized. +func (u *healthCheckerMutableState) Stop() { + u.lock.Lock() + defer u.lock.Unlock() + + // Don't stop the health checker if it has already been stopped. + if u.stopped { + return + } + + u.timer.Stop() + u.stopped = true +} + +// healthChecker encapsulates the logic for monitoring the health of a health-checkable service. +type healthChecker struct { + // mutableState is the mutable state of the health checker. Access to these fields is synchronized. + mutableState *healthCheckerMutableState + + // healthCheckable is the health-checkable service to be monitored. + healthCheckable types.HealthCheckable + + // pollFrequency is the frequency at which the health-checkable service is polled. + pollFrequency time.Duration + + // maxUnhealthyDuration is the maximum acceptable duration for a health-checkable service to + // remain unhealthy. If the service remains unhealthy for this duration, the monitor will execute the + // specified callback function. + maxUnhealthyDuration time.Duration + + // unhealthyCallback is the callback function to be executed if the health-checkable service remains + // unhealthy for a period of time greater than or equal to the maximum acceptable unhealthy duration. + // This callback function is executed with the error that caused the service to become unhealthy. + unhealthyCallback func(error) + + // timeProvider is used to get the current time. It is added as a field so that it can be mocked in tests. + timeProvider libtime.TimeProvider + + logger log.Logger +} + +// Poll executes a health check for the health-checkable service. If the service has been unhealthy for longer than the +// maximum acceptable unhealthy duration, the callback function is executed. +// This method is publicly exposed for testing. This method is synchronized. +func (hc *healthChecker) Poll() { + err := hc.healthCheckable.HealthCheck() + now := hc.timeProvider.Now() + + if err == nil { // Capture healthy response. + hc.mutableState.ReportSuccess(now) + } else { // Capture unhealthy response. + streakDuration := hc.mutableState.ReportFailure(now, err) + // If the service has been unhealthy for longer than the maximum acceptable unhealthy duration, execute the + // callback function. + if streakDuration >= hc.maxUnhealthyDuration { + hc.unhealthyCallback(err) + } + } + + // Schedule next poll. We schedule another poll whether the callback was invoked or not, as callbacks are not + // guaranteed to panic or otherwise halt the daemon. In such cases, we may end up invoking the callback several + // times once the service exceeds the maximum unhealthy duration. For example, a callback that emits error logs + // will continue to emit error logs every 5s until the service becomes healthy again. + hc.mutableState.SchedulePoll(hc.pollFrequency) +} + +// Stop stops the health checker. This method is not synchronized, as the timer does not need synchronization. +func (hc *healthChecker) Stop() { + hc.mutableState.Stop() +} + +// StartNewHealthChecker creates and starts a new health checker for a health-checkable service. +func StartNewHealthChecker( + healthCheckable types.HealthCheckable, + pollFrequency time.Duration, + unhealthyCallback func(error), + timeProvider libtime.TimeProvider, + maxUnhealthyDuration time.Duration, + startupGracePeriod time.Duration, + logger log.Logger, +) *healthChecker { + checker := &healthChecker{ + healthCheckable: healthCheckable, + pollFrequency: pollFrequency, + unhealthyCallback: unhealthyCallback, + timeProvider: timeProvider, + maxUnhealthyDuration: maxUnhealthyDuration, + logger: logger, + mutableState: &healthCheckerMutableState{}, + } + + // The first poll is scheduled after the startup grace period to allow the service to initialize. + // We initialize the timer and schedule a poll outside of object creation in order to avoid data races for + // extremely short startup grace periods. + checker.mutableState.InitializePolling(startupGracePeriod, checker.Poll) + + return checker +} diff --git a/daemons/server/types/health_monitor.go b/daemons/server/types/health_monitor.go new file mode 100644 index 000000000..ebf5e13bb --- /dev/null +++ b/daemons/server/types/health_monitor.go @@ -0,0 +1,239 @@ +package types + +import ( + "fmt" + "sync" + "time" + + cosmoslog "cosmossdk.io/log" + "github.com/cometbft/cometbft/libs/log" + "github.com/tellor-io/layer/daemons/types" + libtime "github.com/tellor-io/layer/lib/time" +) + +const ( + // HealthCheckPollFrequency is the frequency at which the health-checkable service is polled. + HealthCheckPollFrequency = 5 * time.Second + + // HealthMonitorLogModuleName is the module name used for logging within the health monitor. + HealthMonitorLogModuleName = "daemon-health-monitor" +) + +// healthMonitorMutableState tracks all mutable state associated with the health monitor. This state is gathered into +// a single struct for ease of synchronization. +type healthMonitorMutableState struct { + sync.Mutex + + // serviceToHealthChecker maps daemon service names to their update metadata. + serviceToHealthChecker map[string]*healthChecker + // stopped indicates whether the monitor has been stopped. Additional daemon services cannot be registered + // after the monitor has been stopped. + stopped bool + // disabled indicates whether the monitor has been disabled. This is used to disable the monitor in testApp + // tests, where app.New is not executed. + disabled bool +} + +// newHealthMonitorMutableState creates a new health monitor mutable state. +func newHealthMonitorMutableState() *healthMonitorMutableState { + return &healthMonitorMutableState{ + serviceToHealthChecker: make(map[string]*healthChecker), + } +} + +// DisableForTesting disables the health monitor mutable state from receiving updates. This prevents the monitor +// from registering services when called before app initialization and is used for testing. +func (ms *healthMonitorMutableState) DisableForTesting() { + ms.Lock() + defer ms.Unlock() + + ms.disabled = true +} + +// Stop stops the update frequency monitor. This method is synchronized. +func (ms *healthMonitorMutableState) Stop() { + ms.Lock() + defer ms.Unlock() + + // Don't stop the monitor if it has already been stopped. + if ms.stopped { + return + } + + // Stop all health checkers. + for _, checker := range ms.serviceToHealthChecker { + checker.Stop() + } + + ms.stopped = true +} + +// RegisterHealthChecker registers a new health checker for a health-checkable with the health monitor. The health +// checker is lazily created using the provided function if needed. This method is synchronized. It returns an error if +// the service was already registered. +func (ms *healthMonitorMutableState) RegisterHealthChecker( + checkable types.HealthCheckable, + lazyHealthCheckerCreator func() *healthChecker, +) error { + stopService := false + + // If the monitor has already been stopped, we want to stop the checkable service before returning. + // However, we'd prefer not to stop the service within the critical section in order to prevent deadlocks. + // This defer will be called last, after the lock is released. + defer func() { + if stopService { + // If the service is stoppable, stop it. This helps us to clean up daemon services in test cases + // where the monitor is stopped before all daemon services have been registered. + if stoppable, ok := checkable.(Stoppable); ok { + stoppable.Stop() + } + } + }() + + // Enter into the critical section. + ms.Lock() + defer ms.Unlock() + + // Don't register daemon services if the monitor has been disabled. + if ms.disabled { + return nil + } + + // Don't register additional daemon services if the monitor has already been stopped. + // This could be a concern for short-running integration test cases, where the network + // stops before all daemon services have been registered. + if ms.stopped { + // Toggle the stopService flag to true so that the service is stopped after the lock is released. + stopService = true + return nil + } + + if _, ok := ms.serviceToHealthChecker[checkable.ServiceName()]; ok { + return fmt.Errorf("service %v already registered", checkable.ServiceName()) + } + + ms.serviceToHealthChecker[checkable.ServiceName()] = lazyHealthCheckerCreator() + return nil +} + +// HealthMonitor monitors the health of daemon services, which implement the HealthCheckable interface. If a +// registered health-checkable service sustains an unhealthy state for the maximum acceptable unhealthy duration, +// the monitor will execute a callback function. +type HealthMonitor struct { + mutableState *healthMonitorMutableState + + // These fields are initialized in NewHealthMonitor and are not modified after initialization. + logger log.Logger + // startupGracePeriod is the grace period before the monitor starts polling the health-checkable services. + startupGracePeriod time.Duration + // pollingFrequency is the frequency at which the health-checkable services are polled. + pollingFrequency time.Duration + // enablePanics is used to toggle between panics or error logs when a daemon sustains an unhealthy state past the + // maximum allowable duration. + enablePanics bool +} + +// NewHealthMonitor creates a new health monitor. +func NewHealthMonitor( + startupGracePeriod time.Duration, + pollingFrequency time.Duration, + logger log.Logger, + enablePanics bool, +) *HealthMonitor { + return &HealthMonitor{ + mutableState: newHealthMonitorMutableState(), + logger: logger.With(cosmoslog.ModuleKey, HealthMonitorLogModuleName), + startupGracePeriod: startupGracePeriod, + pollingFrequency: pollingFrequency, + enablePanics: enablePanics, + } +} + +func (hm *HealthMonitor) DisableForTesting() { + hm.mutableState.DisableForTesting() +} + +// RegisterServiceWithCallback registers a HealthCheckable with the health monitor. If the service +// stays unhealthy every time it is polled during the maximum acceptable unhealthy duration, the monitor will +// execute the callback function. +// This method is synchronized. The method returns an error if the service was already registered or the +// monitor has already been stopped. If the monitor has been stopped, this method will proactively stop the +// health-checkable service before returning. +func (hm *HealthMonitor) RegisterServiceWithCallback( + hc types.HealthCheckable, + maxUnhealthyDuration time.Duration, + callback func(error), +) error { + if maxUnhealthyDuration <= 0 { + return fmt.Errorf( + "health check registration failure for service %v: "+ + "maximum unhealthy duration %v must be positive", + hc.ServiceName(), + maxUnhealthyDuration, + ) + } + + return hm.mutableState.RegisterHealthChecker(hc, func() *healthChecker { + return StartNewHealthChecker( + hc, + hm.pollingFrequency, + callback, + &libtime.TimeProviderImpl{}, + maxUnhealthyDuration, + hm.startupGracePeriod, + hm.logger, + ) + }) +} + +// PanicServiceNotResponding returns a function that panics with a message indicating that the specified daemon +// service is not responding. This is ideal for creating a callback function when registering a daemon service. +func PanicServiceNotResponding(hc types.HealthCheckable) func(error) { + return func(err error) { + panic(fmt.Sprintf("%v unhealthy: %v", hc.ServiceName(), err)) + } +} + +// LogErrorServiceNotResponding returns a function that logs an error indicating that the specified service +// is not responding. This is ideal for creating a callback function when registering a health-checkable service. +func LogErrorServiceNotResponding(hc types.HealthCheckable, logger log.Logger) func(error) { + return func(err error) { + logger.Error( + "health-checked service is unhealthy", + "service", + hc.ServiceName(), + "error", + err, + ) + } +} + +// RegisterService registers a new health-checkable service with the health check monitor. If the service +// is unhealthy every time it is polled for a duration greater than or equal to the maximum acceptable unhealthy +// duration, the monitor will panic or log an error, depending on the app configuration via the +// `panic-on-daemon-failure-enabled` flag. +// This method is synchronized. It returns an error if the service was already registered or the monitor has +// already been stopped. If the monitor has been stopped, this method will proactively stop the health-checkable +// service before returning. +func (hm *HealthMonitor) RegisterService( + hc types.HealthCheckable, + maxDaemonUnhealthyDuration time.Duration, +) error { + // If the monitor is configured to panic, use the panic callback. Otherwise, use the error log callback. + // This behavior is configured via flag and defaults to panicking on daemon failure. + callback := LogErrorServiceNotResponding(hc, hm.logger) + if hm.enablePanics { + callback = PanicServiceNotResponding(hc) + } + + return hm.RegisterServiceWithCallback( + hc, + maxDaemonUnhealthyDuration, + callback, + ) +} + +// Stop stops the update frequency monitor. This method is synchronized. +func (hm *HealthMonitor) Stop() { + hm.mutableState.Stop() +} diff --git a/mocks/ExchangeToMarketPrices.go b/mocks/ExchangeToMarketPrices.go new file mode 100644 index 000000000..adcca20ce --- /dev/null +++ b/mocks/ExchangeToMarketPrices.go @@ -0,0 +1,74 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// ExchangeToMarketPrices is an autogenerated mock type for the ExchangeToMarketPrices type +type ExchangeToMarketPrices struct { + mock.Mock +} + +// GetAllPrices provides a mock function with given fields: +func (_m *ExchangeToMarketPrices) GetAllPrices() map[string][]types.MarketPriceTimestamp { + ret := _m.Called() + + var r0 map[string][]types.MarketPriceTimestamp + if rf, ok := ret.Get(0).(func() map[string][]types.MarketPriceTimestamp); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]types.MarketPriceTimestamp) + } + } + + return r0 +} + +// GetIndexPrice provides a mock function with given fields: marketId, cutoffTime, resolver +func (_m *ExchangeToMarketPrices) GetIndexPrice(marketId uint32, cutoffTime time.Time, resolver pricefeedtypes.Resolver) (uint64, int) { + ret := _m.Called(marketId, cutoffTime, resolver) + + var r0 uint64 + if rf, ok := ret.Get(0).(func(uint32, time.Time, pricefeedtypes.Resolver) uint64); ok { + r0 = rf(marketId, cutoffTime, resolver) + } else { + r0 = ret.Get(0).(uint64) + } + + var r1 int + if rf, ok := ret.Get(1).(func(uint32, time.Time, pricefeedtypes.Resolver) int); ok { + r1 = rf(marketId, cutoffTime, resolver) + } else { + r1 = ret.Get(1).(int) + } + + return r0, r1 +} + +// UpdatePrice provides a mock function with given fields: exchangeId, marketPriceTimestamp +func (_m *ExchangeToMarketPrices) UpdatePrice(exchangeId string, marketPriceTimestamp *types.MarketPriceTimestamp) { + _m.Called(exchangeId, marketPriceTimestamp) +} + +type mockConstructorTestingTNewExchangeToMarketPrices interface { + mock.TestingT + Cleanup(func()) +} + +// NewExchangeToMarketPrices creates a new instance of ExchangeToMarketPrices. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExchangeToMarketPrices(t mockConstructorTestingTNewExchangeToMarketPrices) *ExchangeToMarketPrices { + mock := &ExchangeToMarketPrices{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/GrpcClient.go b/mocks/GrpcClient.go new file mode 100644 index 000000000..eb6a39dae --- /dev/null +++ b/mocks/GrpcClient.go @@ -0,0 +1,91 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" +) + +// GrpcClient is an autogenerated mock type for the GrpcClient type +type GrpcClient struct { + mock.Mock +} + +// CloseConnection provides a mock function with given fields: grpcConn +func (_m *GrpcClient) CloseConnection(grpcConn *grpc.ClientConn) error { + ret := _m.Called(grpcConn) + + var r0 error + if rf, ok := ret.Get(0).(func(*grpc.ClientConn) error); ok { + r0 = rf(grpcConn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewGrpcConnection provides a mock function with given fields: ctx, socketAddress +func (_m *GrpcClient) NewGrpcConnection(ctx context.Context, socketAddress string) (*grpc.ClientConn, error) { + ret := _m.Called(ctx, socketAddress) + + var r0 *grpc.ClientConn + if rf, ok := ret.Get(0).(func(context.Context, string) *grpc.ClientConn); ok { + r0 = rf(ctx, socketAddress) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*grpc.ClientConn) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, socketAddress) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewTcpConnection provides a mock function with given fields: ctx, endpoint +func (_m *GrpcClient) NewTcpConnection(ctx context.Context, endpoint string) (*grpc.ClientConn, error) { + ret := _m.Called(ctx, endpoint) + + var r0 *grpc.ClientConn + if rf, ok := ret.Get(0).(func(context.Context, string) *grpc.ClientConn); ok { + r0 = rf(ctx, endpoint) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*grpc.ClientConn) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, endpoint) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewGrpcClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewGrpcClient creates a new instance of GrpcClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewGrpcClient(t mockConstructorTestingTNewGrpcClient) *GrpcClient { + mock := &GrpcClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/Logger.go b/mocks/Logger.go new file mode 100644 index 000000000..e9e736c5f --- /dev/null +++ b/mocks/Logger.go @@ -0,0 +1,70 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + log "github.com/cometbft/cometbft/libs/log" + mock "github.com/stretchr/testify/mock" +) + +// Logger is an autogenerated mock type for the Logger type +type Logger struct { + mock.Mock +} + +// Debug provides a mock function with given fields: msg, keyvals +func (_m *Logger) Debug(msg string, keyvals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyvals...) + _m.Called(_ca...) +} + +// Error provides a mock function with given fields: msg, keyvals +func (_m *Logger) Error(msg string, keyvals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyvals...) + _m.Called(_ca...) +} + +// Info provides a mock function with given fields: msg, keyvals +func (_m *Logger) Info(msg string, keyvals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyvals...) + _m.Called(_ca...) +} + +// With provides a mock function with given fields: keyvals +func (_m *Logger) With(keyvals ...interface{}) log.Logger { + var _ca []interface{} + _ca = append(_ca, keyvals...) + ret := _m.Called(_ca...) + + var r0 log.Logger + if rf, ok := ret.Get(0).(func(...interface{}) log.Logger); ok { + r0 = rf(keyvals...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(log.Logger) + } + } + + return r0 +} + +type mockConstructorTestingTNewLogger interface { + mock.TestingT + Cleanup(func()) +} + +// NewLogger creates a new instance of Logger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewLogger(t mockConstructorTestingTNewLogger) *Logger { + mock := &Logger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/PriceFeedMutableMarketConfigs.go b/mocks/PriceFeedMutableMarketConfigs.go new file mode 100644 index 000000000..f231c0c76 --- /dev/null +++ b/mocks/PriceFeedMutableMarketConfigs.go @@ -0,0 +1,108 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricestypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + mock "github.com/stretchr/testify/mock" +) + +// PricefeedMutableMarketConfigs is an autogenerated mock type for the PricefeedMutableMarketConfigs type +type PricefeedMutableMarketConfigs struct { + mock.Mock +} + +// AddPriceEncoder provides a mock function with given fields: updater +func (_m *PricefeedMutableMarketConfigs) AddPriceEncoder(updater types.ExchangeConfigUpdater) { + _m.Called(updater) +} + +// AddPriceFetcher provides a mock function with given fields: updater +func (_m *PricefeedMutableMarketConfigs) AddPriceFetcher(updater types.ExchangeConfigUpdater) { + _m.Called(updater) +} + +// GetExchangeMarketConfigCopy provides a mock function with given fields: id +func (_m *PricefeedMutableMarketConfigs) GetExchangeMarketConfigCopy(id string) (*types.MutableExchangeMarketConfig, error) { + ret := _m.Called(id) + + var r0 *types.MutableExchangeMarketConfig + if rf, ok := ret.Get(0).(func(string) *types.MutableExchangeMarketConfig); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.MutableExchangeMarketConfig) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMarketConfigCopies provides a mock function with given fields: markets +func (_m *PricefeedMutableMarketConfigs) GetMarketConfigCopies(markets []uint32) ([]*types.MutableMarketConfig, error) { + ret := _m.Called(markets) + + var r0 []*types.MutableMarketConfig + if rf, ok := ret.Get(0).(func([]uint32) []*types.MutableMarketConfig); ok { + r0 = rf(markets) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.MutableMarketConfig) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]uint32) error); ok { + r1 = rf(markets) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateMarkets provides a mock function with given fields: marketParams +func (_m *PricefeedMutableMarketConfigs) UpdateMarkets(marketParams []pricestypes.MarketParam) (map[uint32]error, error) { + ret := _m.Called(marketParams) + + var r0 map[uint32]error + if rf, ok := ret.Get(0).(func([]pricestypes.MarketParam) map[uint32]error); ok { + r0 = rf(marketParams) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[uint32]error) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]pricestypes.MarketParam) error); ok { + r1 = rf(marketParams) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewPricefeedMutableMarketConfigs interface { + mock.TestingT + Cleanup(func()) +} + +// NewPricefeedMutableMarketConfigs creates a new instance of PricefeedMutableMarketConfigs. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewPricefeedMutableMarketConfigs(t mockConstructorTestingTNewPricefeedMutableMarketConfigs) *PricefeedMutableMarketConfigs { + mock := &PricefeedMutableMarketConfigs{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/QueryClient.go b/mocks/QueryClient.go new file mode 100644 index 000000000..82db44be0 --- /dev/null +++ b/mocks/QueryClient.go @@ -0,0 +1,69 @@ +// 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/tellor-io/layer/daemons/server/types" +) + +// QueryClient is an autogenerated mock type for the PriceFeedServiceClient type +type QueryClient struct { + mock.Mock +} + +// UpdateMarketPrices provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) UpdateMarketPrices(ctx context.Context, in *types.UpdateMarketPricesRequest, opts ...grpc.CallOption) (*types.UpdateMarketPricesResponse, 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 UpdateMarketPrices") + } + + var r0 *types.UpdateMarketPricesResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) (*types.UpdateMarketPricesResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) *types.UpdateMarketPricesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.UpdateMarketPricesResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewQueryClient creates a new instance of QueryClient. 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 NewQueryClient(t interface { + mock.TestingT + Cleanup(func()) +}) *QueryClient { + mock := &QueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/testutil/appoptions/app_options.go b/testutil/appoptions/app_options.go new file mode 100644 index 000000000..691e4e78d --- /dev/null +++ b/testutil/appoptions/app_options.go @@ -0,0 +1,72 @@ +package appoptions + +import ( + "fmt" + "os" + + "github.com/cosmos/cosmos-sdk/server/config" + appflags "github.com/tellor-io/layer/app/flags" + + "github.com/cosmos/cosmos-sdk/client/flags" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + daemonflags "github.com/tellor-io/layer/daemons/flags" +) + +// FakeAppOptions is a helper struct used for creating `servertypes.AppOptions` for simulator and end-to-end testing. +// This struct allows for customizing the `servertypes.AppOptions` value that is normally supplied from CLI arguments +// to `layerd`. The real concrete implementation for this interface is in the "viper" package which is used +// under the hood by "cobra", which is the CLI framework used by Cosmos SDK. +type FakeAppOptions struct { + options map[string]interface{} +} + +func NewFakeAppOptions() *FakeAppOptions { + return &FakeAppOptions{ + options: make(map[string]interface{}), + } +} + +func (fao *FakeAppOptions) Set(option string, value interface{}) { + fao.options[option] = value +} + +// Get implements the `servertypes.AppOptions` interface. +func (fao *FakeAppOptions) Get(o string) interface{} { + value, ok := fao.options[o] + if !ok { + return nil + } + + return value +} + +// GetDefaultTestAppOptions returns a default set of AppOptions with the daemons disabled for end-to-end +// and simulator testing. +func GetDefaultTestAppOptions(homePath string, customFlags map[string]interface{}) servertypes.AppOptions { + fao := NewFakeAppOptions() + + fao.Set(flags.FlagHome, homePath) + + // Disable the Price Daemon for all end-to-end and integration tests by default. + fao.Set(daemonflags.FlagPriceDaemonEnabled, false) + + // Populate the default value for gRPC. + fao.Set(appflags.GrpcAddress, config.DefaultGRPCAddress) + + for flag, value := range customFlags { + fao.Set(flag, value) + } + return fao +} + +func GetDefaultTestAppOptionsFromTempDirectory( + homePath string, + customFlags map[string]interface{}, +) servertypes.AppOptions { + dir, err := os.MkdirTemp(homePath, "testapp") + if err != nil { + panic(fmt.Sprintf("failed creating temporary directory: %v", err)) + } + defer os.RemoveAll(dir) + return GetDefaultTestAppOptions(".", customFlags) +} diff --git a/testutil/client/exchange_id_to_price_timestamp.go b/testutil/client/exchange_id_to_price_timestamp.go new file mode 100644 index 000000000..9cf2d71ad --- /dev/null +++ b/testutil/client/exchange_id_to_price_timestamp.go @@ -0,0 +1,13 @@ +package client + +import ( + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// ExchangeIdMarketPriceTimestamp contains an `ExchangeId` and an associated +// `types.MarketPriceTimestamp`. This type exists for convenience and clarity in testing the +// pricefeed client. +type ExchangeIdMarketPriceTimestamp struct { + ExchangeId types.ExchangeId + MarketPriceTimestamp *types.MarketPriceTimestamp +} diff --git a/testutil/constants/price_feed.go b/testutil/constants/price_feed.go new file mode 100644 index 000000000..7fb4ec959 --- /dev/null +++ b/testutil/constants/price_feed.go @@ -0,0 +1,689 @@ +package constants + +import ( + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + daemonClientTypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricefeedclienttypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + servertypes "github.com/tellor-io/layer/daemons/server/types" + "github.com/tellor-io/layer/testutil/client" +) + +var ( + // Markets + MarketId0 = uint32(0) + MarketId1 = uint32(1) + MarketId2 = uint32(2) + + MarketId7 = uint32(7) + MarketId8 = uint32(8) + MarketId9 = uint32(9) + MarketId10 = uint32(10) + MarketId11 = uint32(11) + + // Exponents + Exponent9 = int32(-9) + Exponent8 = int32(-8) + Exponent7 = int32(-7) + + // Exchanges + ExchangeId0 = "Exchange0" + ExchangeId1 = "Exchange1" + ExchangeId2 = "Exchange2" + ExchangeId3 = "Exchange3" + + // ExchangeArray + Exchange1Exchange2Array = []string{ + ExchangeId1, + ExchangeId2, + } + + // MarketPriceTimestamps + Market8_TimeTMinusThreshold_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId8, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price1, + } + Market8_TimeTMinusThreshold_Price2 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId8, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price2, + } + Market8_TimeTMinusThreshold_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId8, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price3, + } + Market8_TimeT_Price2 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId8, + LastUpdatedAt: TimeT, + Price: Price2, + } + Market8_TimeT_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId8, + LastUpdatedAt: TimeT, + Price: Price3, + } + Market8_TimeT_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId8, + LastUpdatedAt: TimeT, + Price: Price1, + } + Market9_TimeTMinusThreshold_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price1, + } + Market9_TimeTMinusThreshold_Price2 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price2, + } + Market9_TimeTMinusThreshold_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price3, + } + Market9_TimeT_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeT, + Price: Price1, + } + Market9_TimeT_Price2 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeT, + Price: Price2, + } + Market9_TimeT_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeT, + Price: Price3, + } + Market9_TimeTPlusThreshold_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeTPlusThreshold, + Price: Price1, + } + Market9_TimeTPlusThreshold_Price2 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeTPlusThreshold, + Price: Price2, + } + Market9_TimeTPlusThreshold_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId9, + LastUpdatedAt: TimeTPlusThreshold, + Price: Price3, + } + Market10_TimeT_Price4 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId10, + LastUpdatedAt: TimeT, + Price: Price4, + } + Market11_TimeT_Price5 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId11, + LastUpdatedAt: TimeT, + Price: Price5, + } + Market7_BeforeTimeT_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId7, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price1, + } + Market7_BeforeTimeT_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId7, + LastUpdatedAt: TimeTMinusThreshold, + Price: Price3, + } + Market7_TimeT_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId7, + LastUpdatedAt: TimeT, + Price: Price1, + } + Market7_TimeT_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId7, + LastUpdatedAt: TimeT, + Price: Price3, + } + Market7_TimeTPlusThreshold_Price1 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId7, + LastUpdatedAt: TimeTPlusThreshold, + Price: Price1, + } + Market7_TimeTPlusThreshold_Price3 = &daemonClientTypes.MarketPriceTimestamp{ + MarketId: MarketId7, + LastUpdatedAt: TimeTPlusThreshold, + Price: Price3, + } + + // Prices + InvalidPrice uint64 = 0 + Price1 uint64 = 1001 + Price2 uint64 = 2002 + Price3 uint64 = 3003 + Price4 uint64 = 4004 + Price5 uint64 = 500005 + Price6 uint64 = 60006 + Price7 uint64 = 7007 + + // Exchange 0 prices + Exchange0_Price4_TimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId1, + Price: Price4, + LastUpdateTime: &TimeT, + } + + // Exchange 1 prices + Exchange1_Price1_TimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId1, + Price: Price1, + LastUpdateTime: &TimeT, + } + Exchange1_Price2_AfterTimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId1, + Price: Price2, + LastUpdateTime: &TimeTPlusThreshold, + } + Exchange1_Price3_BeforeTimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId1, + Price: Price3, + LastUpdateTime: &TimeTMinusThreshold, + } + + // Exchange 2 prices + Exchange2_Price2_TimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId2, + Price: Price2, + LastUpdateTime: &TimeT, + } + Exchange2_Price3_AfterTimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId2, + Price: Price3, + LastUpdateTime: &TimeTPlusThreshold, + } + Exchange2_Price1_BeforeTimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId2, + Price: Price1, + LastUpdateTime: &TimeTMinusThreshold, + } + + // Exchange 3 prices + Exchange3_Price3_TimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId3, + Price: Price3, + LastUpdateTime: &TimeT, + } + Exchange3_Price4_AfterTimeT = &servertypes.ExchangePrice{ + ExchangeId: ExchangeId3, + Price: Price4, + LastUpdateTime: &TimeTPlusThreshold, + } + + // Price Updates + Market9_SingleExchange_AtTimeUpdate = []*servertypes.MarketPriceUpdate{ + { + MarketId: MarketId9, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price1_TimeT, + }, + }, + } + AtTimeTPriceUpdate = []*servertypes.MarketPriceUpdate{ + { + MarketId: MarketId9, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price1_TimeT, + Exchange2_Price2_TimeT, + }, + }, + { + MarketId: MarketId8, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange2_Price2_TimeT, + Exchange3_Price3_TimeT, + }, + }, + { + MarketId: MarketId7, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price1_TimeT, + Exchange3_Price3_TimeT, + }, + }, + } + + AtTimeTSingleExchangePriceUpdate = []*servertypes.MarketPriceUpdate{ + { + MarketId: MarketId0, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange0_Price4_TimeT, + }, + }, + { + MarketId: MarketId1, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price1_TimeT, + }, + }, + { + MarketId: MarketId2, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange2_Price2_TimeT, + }, + }, + } + AtTimeTSingleExchangeSmoothedPrices = map[uint32]uint64{ + MarketId0: Exchange0_Price4_TimeT.Price, + MarketId1: Exchange1_Price1_TimeT.Price, + MarketId2: Exchange2_Price2_TimeT.Price, + } + + AtTimeTSingleExchangeSmoothedPricesPlus10 = map[uint32]uint64{ + MarketId0: Exchange0_Price4_TimeT.Price + 10, + MarketId1: Exchange1_Price1_TimeT.Price + 10, + MarketId2: Exchange2_Price2_TimeT.Price + 10, + } + + AtTimeTSingleExchangeSmoothedPricesPlus7 = map[uint32]uint64{ + MarketId0: Exchange0_Price4_TimeT.Price + 7, + MarketId1: Exchange1_Price1_TimeT.Price + 7, + MarketId2: Exchange2_Price2_TimeT.Price + 7, + } + + MixedTimePriceUpdate = []*servertypes.MarketPriceUpdate{ + { + MarketId: MarketId9, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price1_TimeT, + Exchange2_Price2_TimeT, + Exchange3_Price3_TimeT, + }, + }, + { + MarketId: MarketId8, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price3_BeforeTimeT, + Exchange2_Price2_TimeT, + Exchange3_Price3_TimeT, + }, + }, + { + MarketId: MarketId7, + ExchangePrices: []*servertypes.ExchangePrice{ + Exchange1_Price3_BeforeTimeT, + Exchange2_Price1_BeforeTimeT, + Exchange3_Price3_TimeT, + }, + }, + } + + // Markets + AllMarketParamsMinExchanges2 = []pricefeedclienttypes.MarketParam{ + { + Id: MarketId9, + Exponent: Exponent9, + MinExchanges: 2, + }, + { + Id: MarketId8, + Exponent: Exponent8, + MinExchanges: 2, + }, + { + Id: MarketId7, + Exponent: Exponent7, + MinExchanges: 2, + }, + } + AllMarketParamsMinExchanges3 = []pricefeedclienttypes.MarketParam{ + { + Id: MarketId9, + MinExchanges: 3, + }, + { + Id: MarketId8, + MinExchanges: 3, + }, + { + Id: MarketId7, + MinExchanges: 3, + }, + } + + // ExchangeConfig, MutableExchangeMarketConfig for various tests are defined below. + + SingleMarketExchangeQueryDetails = daemonClientTypes.ExchangeQueryDetails{IsMultiMarket: false} + MultiMarketExchangeQueryDetails = daemonClientTypes.ExchangeQueryDetails{IsMultiMarket: true} + + // ExchangeQueryConfigs. + Exchange1_0MaxQueries_QueryConfig = daemonClientTypes.ExchangeQueryConfig{ + ExchangeId: ExchangeId1, + IntervalMs: 100, + TimeoutMs: 3_000, + MaxQueries: 0, + } + + Exchange1_1MaxQueries_QueryConfig = daemonClientTypes.ExchangeQueryConfig{ + ExchangeId: ExchangeId1, + IntervalMs: 100, + TimeoutMs: 3_000, + MaxQueries: 1, + } + + Exchange1_2MaxQueries_QueryConfig = daemonClientTypes.ExchangeQueryConfig{ + ExchangeId: ExchangeId1, + IntervalMs: 100, + TimeoutMs: 3_000, + MaxQueries: 2, + } + + // MutableExchangeMarketConfigs for 0, 1, 2, 3, and 5 markets. + Exchange1_NoMarkets_MutableExchangeMarketConfig = daemonClientTypes.MutableExchangeMarketConfig{ + Id: ExchangeId1, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{}, + } + + Exchange1_1Markets_MutableExchangeMarketConfig = daemonClientTypes.MutableExchangeMarketConfig{ + Id: ExchangeId1, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{ + MarketId7: { + Ticker: "BTC-USD", + }, + }, + } + + Exchange1_2Markets_MutableExchangeMarketConfig = daemonClientTypes.MutableExchangeMarketConfig{ + Id: ExchangeId1, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{ + MarketId7: { + Ticker: "BTC-USD", + }, + MarketId8: { + Ticker: "ETH-USD", + }, + }, + } + + Exchange1_3Markets_MutableExchangeMarketConfig = daemonClientTypes.MutableExchangeMarketConfig{ + Id: ExchangeId1, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{ + MarketId7: { + Ticker: "BTC-USD", + }, + MarketId8: { + Ticker: "ETH-USD", + }, + MarketId9: { + Ticker: "LTC-USD", + }, + }, + } + + Exchange1_5Markets_MutableExchangeMarketConfig = daemonClientTypes.MutableExchangeMarketConfig{ + Id: ExchangeId1, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{ + MarketId7: { + Ticker: "BTC-USD", + }, + MarketId8: { + Ticker: "ETH-USD", + }, + MarketId9: { + Ticker: "LTC-USD", + }, + MarketId10: { + Ticker: "XRP-USD", + }, + MarketId11: { + Ticker: "BCH-USD", + }, + }, + } + + CanonicalMarketExponents = map[daemonClientTypes.MarketId]daemonClientTypes.Exponent{ + MarketId7: MutableMarketConfigs_5Markets[0].Exponent, + MarketId8: MutableMarketConfigs_5Markets[1].Exponent, + MarketId9: MutableMarketConfigs_5Markets[2].Exponent, + MarketId10: MutableMarketConfigs_5Markets[3].Exponent, + MarketId11: MutableMarketConfigs_5Markets[4].Exponent, + } + + CanonicalMarketPriceTimestampResponses = map[uint32]*daemonClientTypes.MarketPriceTimestamp{ + MarketId7: Market7_TimeTPlusThreshold_Price3, + MarketId8: Market8_TimeTMinusThreshold_Price2, + MarketId9: Market9_TimeT_Price1, + MarketId10: Market10_TimeT_Price4, + MarketId11: Market11_TimeT_Price5, + } + + // ExchangeIdMarketPriceTimestamps + ExchangeId1_Market9_TimeT_Price1 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId1, + MarketPriceTimestamp: Market9_TimeT_Price1, + } + ExchangeId2_Market9_TimeT_Price2 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId2, + MarketPriceTimestamp: Market9_TimeT_Price2, + } + ExchangeId3_Market9_TimeT_Price3 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId3, + MarketPriceTimestamp: Market9_TimeT_Price3, + } + ExchangeId1_Market8_BeforeTimeT_Price3 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId1, + MarketPriceTimestamp: Market8_TimeTMinusThreshold_Price3, + } + ExchangeId2_Market8_TimeT_Price2 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId2, + MarketPriceTimestamp: Market8_TimeT_Price2, + } + ExchangeId3_Market8_TimeT_Price3 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId3, + MarketPriceTimestamp: Market8_TimeT_Price3, + } + ExchangeId1_Market7_BeforeTimeT_Price3 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId1, + MarketPriceTimestamp: Market7_BeforeTimeT_Price3, + } + ExchangeId2_Market7_BeforeTimeT_Price1 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId2, + MarketPriceTimestamp: Market7_BeforeTimeT_Price1, + } + ExchangeId1_Market7_TimeT_Price1 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId1, + MarketPriceTimestamp: Market7_TimeT_Price1, + } + ExchangeId3_Market7_TimeT_Price3 = &client.ExchangeIdMarketPriceTimestamp{ + ExchangeId: ExchangeId3, + MarketPriceTimestamp: Market7_TimeT_Price3, + } + + CoinbaseMutableMarketConfig = &daemonClientTypes.MutableExchangeMarketConfig{ + Id: CoinbaseExchangeName, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{ + MarketId7: { + Ticker: "BTC-USD", + }, + MarketId8: { + Ticker: "ETH-USD", + }, + }, + } + BinanceMutableMarketConfig = &daemonClientTypes.MutableExchangeMarketConfig{ + Id: BinanceExchangeName, + MarketToMarketConfig: map[daemonClientTypes.MarketId]daemonClientTypes.MarketConfig{ + MarketId7: { + Ticker: "BTCUSDT", + }, + MarketId8: { + Ticker: "ETHUSDT", + }, + }, + } + + TestCanonicalExchangeIds = []string{ExchangeId1, ExchangeId2} + + // Test constants for starting the daemon client. + TestMutableExchangeMarketConfigs = map[string]*daemonClientTypes.MutableExchangeMarketConfig{ + CoinbaseExchangeName: CoinbaseMutableMarketConfig, + BinanceExchangeName: BinanceMutableMarketConfig, + } + + TestMarket7And8Params = []types.MarketParam{ + { + Id: 7, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + ExchangeConfigJson: `{"exchanges":[{"exchangeName":"Binance","ticker":"BTCUSDT"},` + + `{"exchangeName":"Coinbase","ticker":"BTC-USD"}]}`, + MinExchanges: 1, + MinPriceChangePpm: 50, + }, + { + Id: 8, + Pair: EthUsdPair, + Exponent: EthUsdExponent, + MinExchanges: 1, + ExchangeConfigJson: `{"exchanges":[{"exchangeName":"Binance","ticker":"ETHUSDT"},` + + `{"exchangeName":"Coinbase","ticker":"ETH-USD"}]}`, + MinPriceChangePpm: 50, + }, + } + + TestMutableMarketConfigs = map[daemonClientTypes.MarketId]*daemonClientTypes.MutableMarketConfig{ + MarketId7: { + Id: MarketId7, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + MinExchanges: 1, + }, + MarketId8: { + Id: MarketId8, + Pair: EthUsdPair, + Exponent: EthUsdExponent, + MinExchanges: 1, + }, + } + + // Pricefetcher MutableMarketConfigs for 0, 1, 2, 3 and 5 markets. + MutableMarketConfigs_0Markets = []*daemonClientTypes.MutableMarketConfig{} + + MutableMarketConfigs_1Markets = []*daemonClientTypes.MutableMarketConfig{ + { + Id: MarketId7, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + MinExchanges: 1, + }, + } + + MutableMarketConfigs_2Markets = []*daemonClientTypes.MutableMarketConfig{ + { + Id: MarketId7, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId8, + Pair: EthUsdPair, + Exponent: EthUsdExponent, + MinExchanges: 1, + }, + } + + MutableMarketConfigs_3Markets = []*daemonClientTypes.MutableMarketConfig{ + { + Id: MarketId7, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId8, + Pair: EthUsdPair, + Exponent: EthUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId9, + Pair: LtcUsdPair, + Exponent: LtcUsdExponent, + MinExchanges: 1, + }, + } + + MutableMarketConfigs_5Markets = []*daemonClientTypes.MutableMarketConfig{ + { + Id: MarketId7, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId8, + Pair: EthUsdPair, + Exponent: EthUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId9, + Pair: LtcUsdPair, + Exponent: LtcUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId10, + Pair: SolUsdPair, + Exponent: SolUsdExponent, + MinExchanges: 1, + }, + { + Id: MarketId11, + Pair: MaticUsdPair, + Exponent: MaticUsdExponent, + MinExchanges: 1, + }, + } + + MarketToMutableMarketConfigs_5Markets = map[daemonClientTypes.MarketId]*daemonClientTypes.MutableMarketConfig{ + MarketId7: MutableMarketConfigs_5Markets[0], + MarketId8: MutableMarketConfigs_5Markets[1], + MarketId9: MutableMarketConfigs_5Markets[2], + MarketId10: MutableMarketConfigs_5Markets[3], + MarketId11: MutableMarketConfigs_5Markets[4], + } + + // Expected exponents for above configs. + MutableMarketConfigs_3Markets_ExpectedExponents = map[daemonClientTypes.MarketId]daemonClientTypes.Exponent{ + MarketId7: BtcUsdExponent, + MarketId8: EthUsdExponent, + MarketId9: LtcUsdExponent, + } + + MutableMarketConfigs_5Markets_ExpectedExponents = map[daemonClientTypes.MarketId]daemonClientTypes.Exponent{ + MarketId7: BtcUsdExponent, + MarketId8: EthUsdExponent, + MarketId9: LtcUsdExponent, + MarketId10: SolUsdExponent, + MarketId11: MaticUsdExponent, + } + + TestExchangeQueryConfigs = map[string]*daemonClientTypes.ExchangeQueryConfig{ + ExchangeId1: { + ExchangeId: ExchangeId1, + IntervalMs: 100, + TimeoutMs: 3_000, + MaxQueries: 2, + }, + ExchangeId2: { + ExchangeId: ExchangeId2, + IntervalMs: 100, + TimeoutMs: 3_000, + MaxQueries: 2, + }, + } + TestExchangeIdToExchangeQueryDetails = map[string]daemonClientTypes.ExchangeQueryDetails{ + ExchangeId1: constants.StaticExchangeDetails[ExchangeId1], + ExchangeId2: constants.StaticExchangeDetails[ExchangeId2], + } +) diff --git a/testutil/constants/prices.go b/testutil/constants/prices.go index a2e4dad59..0f2615a17 100644 --- a/testutil/constants/prices.go +++ b/testutil/constants/prices.go @@ -1,6 +1,216 @@ package constants +import ( + pricefeedclient "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/testutil/daemons/pricefeed/exchange_config" +) + const ( - BtcUsdExponent = -5 - EthUsdExponent = -6 + BtcUsdPair = "BTC-USD" + EthUsdPair = "ETH-USD" + MaticUsdPair = "MATIC-USD" + SolUsdPair = "SOL-USD" + LtcUsdPair = "LTC-USD" + + BtcUsdExponent = -5 + EthUsdExponent = -6 + LinkUsdExponent = -8 + MaticUsdExponent = -9 + CrvUsdExponent = -10 + SolUsdExponent = -8 + LtcUsdExponent = -7 + + CoinbaseExchangeName = "Coinbase" + BinanceExchangeName = "Binance" + BinanceUSExchangeName = "BinanceUS" + BitfinexExchangeName = "Bitfinex" + KrakenExchangeName = "Kraken" + + FiveBillion = uint64(5_000_000_000) + ThreeBillion = uint64(3_000_000_000) + FiveMillion = uint64(5_000_000) + OneMillion = uint64(1_000_000) + + // Market param validation errors. + ErrorMsgMarketPairCannotBeEmpty = "Pair cannot be empty" + ErrorMsgInvalidMinPriceChange = "Min price change in parts-per-million must be greater than 0 and less than 10000" ) + +var TestMarketExchangeConfigs = map[pricefeedclient.MarketId]string{ + exchange_config.MARKET_BTC_USD: `{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "BTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "BinanceUS", + "ticker": "BTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitfinex", + "ticker": "tBTCUSD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "BTC/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "BTC-USD" + }, + { + "exchangeName": "CryptoCom", + "ticker": "BTC_USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XXBTZUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "BTC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BTC-USDT", + "adjustByMarket": "USDT-USD" + } + ] + }`, + exchange_config.MARKET_ETH_USD: `{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "ETHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "BinanceUS", + "ticker": "ETHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitfinex", + "ticker": "tETHUSD" + }, + { + "exchangeName": "Bitstamp", + "ticker": "ETH/USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ETHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ETH-USD" + }, + { + "exchangeName": "CryptoCom", + "ticker": "ETH_USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XETHZUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "ETH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ETH-USDT", + "adjustByMarket": "USDT-USD" + } + ] + }`, + exchange_config.MARKET_SOL_USD: `{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "SOLUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bitfinex", + "ticker": "tSOLUSD", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SOLUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SOL-USD" + }, + { + "exchangeName": "CryptoCom", + "ticker": "SOL_USD" + }, + { + "exchangeName": "Huobi", + "ticker": "solusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SOLUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SOL-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SOL_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SOL-USDT", + "adjustByMarket": "USDT-USD" + } + ] + }`, +} + +var TestMarketParams = []pricefeedclient.MarketParam{ + { + Id: 0, + Pair: BtcUsdPair, + Exponent: BtcUsdExponent, + MinExchanges: 1, + MinPriceChangePpm: 50, + ExchangeConfigJson: TestMarketExchangeConfigs[exchange_config.MARKET_BTC_USD], + }, + { + Id: 1, + Pair: EthUsdPair, + Exponent: EthUsdExponent, + MinExchanges: 1, + MinPriceChangePpm: 50, + ExchangeConfigJson: TestMarketExchangeConfigs[exchange_config.MARKET_ETH_USD], + }, + { + Id: 2, + Pair: SolUsdPair, + Exponent: SolUsdExponent, + MinExchanges: 1, + MinPriceChangePpm: 50, + ExchangeConfigJson: TestMarketExchangeConfigs[exchange_config.MARKET_SOL_USD], + }, +} diff --git a/testutil/constants/time.go b/testutil/constants/time.go new file mode 100644 index 000000000..33df0917d --- /dev/null +++ b/testutil/constants/time.go @@ -0,0 +1,23 @@ +package constants + +import ( + "time" + + "github.com/tellor-io/layer/daemons/constants" +) + +var ( + // Time + TimeZero = time.Date(1, time.Month(1), 1, 0, 0, 0, 0, time.UTC) + TimeTen = time.Unix(10, 0) + TimeFifteen = time.Unix(15, 0) + TimeTwenty = time.Unix(20, 0) + TimeTwentyFive = time.Unix(25, 0) + TimeThirty = time.Unix(30, 0) + TimeT = time.Unix(1650000000, 0) // 2022-04-14 22:20:00 -0700 PDT + TimeTMinus1 = TimeT.Add(-time.Duration(1)) + TimeTPlus1 = TimeT.Add(time.Duration(1)) + TimeTMinusThreshold = TimeT.Add(-constants.MaxPriceAge).Add(-time.Duration(1)) + TimeTPlusThreshold = TimeT.Add(constants.MaxPriceAge).Add(time.Duration(1)) + Time_21st_Feb_2021 = time.Date(2021, time.Month(2), 21, 0, 0, 0, 0, time.UTC) +) diff --git a/testutil/daemons/common.go b/testutil/daemons/common.go new file mode 100644 index 000000000..870fafeda --- /dev/null +++ b/testutil/daemons/common.go @@ -0,0 +1,32 @@ +package daemons + +import "time" + +// SingleTickTickerAndStop creates a ticker that ticks once before the stop channel is signaled. +func SingleTickTickerAndStop() (*time.Ticker, chan bool) { + // Create a ticker with a duration long enough that we do not expect to see a tick within the timeframe + // of a normal unit test. + ticker := time.NewTicker(10 * time.Minute) + // Override the ticker's channel with a new channel we can insert into directly, and add a single tick. + newChan := make(chan time.Time, 1) + newChan <- time.Now() + ticker.C = newChan + + stop := make(chan bool, 1) + + // Start a go-routine that will signal the stop channel once the single tick is consumed. + go func() { + for { + // Once the single tick is consumed, stop the ticker and signal the stop channel. + if len(ticker.C) == 0 { + stop <- true + close(stop) + ticker.Stop() + return + } + time.Sleep(10 * time.Millisecond) + } + }() + + return ticker, stop +} diff --git a/testutil/daemons/pricefeed/exchange_config/static_market_names.go b/testutil/daemons/pricefeed/exchange_config/static_market_names.go new file mode 100644 index 000000000..3928e27b5 --- /dev/null +++ b/testutil/daemons/pricefeed/exchange_config/static_market_names.go @@ -0,0 +1,46 @@ +package exchange_config + +import ( + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +var ( + // StaticMarketNames maps marketIds to their human-readable market names. This list is + // used for generating market exchange config that is then read back into the daemon. + StaticMarketNames = map[types.MarketId]string{ + MARKET_BTC_USD: "BTC-USD", + MARKET_ETH_USD: "ETH-USD", + MARKET_LINK_USD: "LINK-USD", + MARKET_MATIC_USD: "MATIC-USD", + MARKET_CRV_USD: "CRV-USD", + MARKET_SOL_USD: "SOL-USD", + MARKET_ADA_USD: "ADA-USD", + MARKET_AVAX_USD: "AVAX-USD", + MARKET_FIL_USD: "FIL-USD", + MARKET_LTC_USD: "LTC-USD", + MARKET_DOGE_USD: "DOGE-USD", + MARKET_ATOM_USD: "ATOM-USD", + MARKET_DOT_USD: "DOT-USD", + MARKET_UNI_USD: "UNI-USD", + MARKET_BCH_USD: "BCH-USD", + MARKET_TRX_USD: "TRX-USD", + MARKET_NEAR_USD: "NEAR-USD", + MARKET_MKR_USD: "MKR-USD", + MARKET_XLM_USD: "XLM-USD", + MARKET_ETC_USD: "ETC-USD", + MARKET_COMP_USD: "COMP-USD", + MARKET_WLD_USD: "WLD-USD", + MARKET_APE_USD: "APE-USD", + MARKET_APT_USD: "APT-USD", + MARKET_ARB_USD: "ARB-USD", + MARKET_BLUR_USD: "BLUR-USD", + MARKET_LDO_USD: "LDO-USD", + MARKET_OP_USD: "OP-USD", + MARKET_PEPE_USD: "PEPE-USD", + MARKET_SEI_USD: "SEI-USD", + MARKET_SHIB_USD: "SHIB-USD", + MARKET_SUI_USD: "SUI-USD", + MARKET_XRP_USD: "XRP-USD", + MARKET_USDT_USD: "USDT-USD", + } +) diff --git a/testutil/daemons/pricefeed/pricefeed_utils.go b/testutil/daemons/pricefeed/pricefeed_utils.go index cce44cf87..62a4f2ade 100644 --- a/testutil/daemons/pricefeed/pricefeed_utils.go +++ b/testutil/daemons/pricefeed/pricefeed_utils.go @@ -7,9 +7,32 @@ import ( "os" "testing" + "golang.org/x/exp/maps" + "github.com/stretchr/testify/require" + // "github.com/tellor-io/layer/testutil/json" ) +// ErrorMapsEqual is a testing method that takes any two maps of keys to errors and asserts that they have the same +// sets of keys, and that each associated error value has the same rendered message. +func ErrorMapsEqual[K comparable](t *testing.T, expected map[K]error, actual map[K]error) { + require.Equal(t, len(expected), len(actual)) + for key, expectedError := range expected { + error, ok := actual[key] + require.True(t, ok) + require.EqualError(t, error, expectedError.Error()) + } +} + +// ErrorsEqual is a testing method that takes any two slices of errors and asserts that each actual error has +// the same rendered message as the expected error. +func ErrorsEqual(t *testing.T, expected []error, actual []error) { + require.Equal(t, len(expected), len(actual)) + for i, expectedError := range expected { + require.EqualError(t, expectedError, actual[i].Error()) + } +} + // ReadJsonTestFile takes a test file with human-readable, formatted JSON, load it, and compacts it. // The purpose is to remove the formatting (e.g. newlines, tabs, etc) and return a string that would match an // unmarshaled object string generated by a Go program natively. @@ -18,7 +41,6 @@ func ReadJsonTestFile(t *testing.T, fileName string) string { require.NoError(t, err, "Error reading test file") return CompactJsonString(t, string(fileBytes)) } - func CompactJsonString(t *testing.T, jsonString string) string { buffer := &bytes.Buffer{} err := json.Compact(buffer, []byte(jsonString)) @@ -26,13 +48,22 @@ func CompactJsonString(t *testing.T, jsonString string) string { return buffer.String() } -// ErrorMapsEqual is a testing method that takes any two maps of keys to errors and asserts that they have the same -// sets of keys, and that each associated error value has the same rendered message. -func ErrorMapsEqual[K comparable](t *testing.T, expected map[K]error, actual map[K]error) { - require.Equal(t, len(expected), len(actual)) - for key, expectedError := range expected { - error, ok := actual[key] - require.True(t, ok) - require.EqualError(t, error, expectedError.Error()) +// MarketParamErrorsEqual is a testing method that takes any two maps of market ids to errors and asserts that they +// have the same sets of keys, and that each associated error value has the same rendered message +func MarketParamErrorsEqual( + t *testing.T, + expectedMarketParamErrors map[uint32]error, + actualMarketParamErrors map[uint32]error, +) { + require.Equal(t, maps.Keys(expectedMarketParamErrors), maps.Keys(actualMarketParamErrors)) + for marketId, expectedErr := range expectedMarketParamErrors { + actualErr := actualMarketParamErrors[marketId] + require.ErrorContains( + t, + actualErr, + expectedErr.Error(), + "Errors for market id %v do not match", + marketId, + ) } } diff --git a/testutil/encoding/utils.go b/testutil/encoding/utils.go new file mode 100644 index 000000000..a27e469bf --- /dev/null +++ b/testutil/encoding/utils.go @@ -0,0 +1,67 @@ +package encoding + +import ( + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/capability" + "github.com/cosmos/cosmos-sdk/x/consensus" + "github.com/cosmos/cosmos-sdk/x/crisis" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + "github.com/cosmos/cosmos-sdk/x/params" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" + "github.com/cosmos/ibc-go/v7/modules/apps/transfer" + ibc "github.com/cosmos/ibc-go/v7/modules/core" + ibcclientclient "github.com/cosmos/ibc-go/v7/modules/core/02-client/client" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + + "github.com/stretchr/testify/require" +) + +// GetTestEncodingCfg returns an encoding config for testing purposes. +func GetTestEncodingCfg() testutil.TestEncodingConfig { + encodingCfg := testutil.MakeTestEncodingConfig( + auth.AppModuleBasic{}, + genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), + bank.AppModuleBasic{}, + capability.AppModuleBasic{}, + staking.AppModuleBasic{}, + distr.AppModuleBasic{}, + gov.NewAppModuleBasic( + []govclient.ProposalHandler{ + paramsclient.ProposalHandler, + upgradeclient.LegacyProposalHandler, + upgradeclient.LegacyCancelProposalHandler, + ibcclientclient.UpdateClientProposalHandler, + ibcclientclient.UpgradeProposalHandler, + }, + ), + params.AppModuleBasic{}, + crisis.AppModuleBasic{}, + ibc.AppModuleBasic{}, + ibctm.AppModuleBasic{}, + upgrade.AppModuleBasic{}, + transfer.AppModuleBasic{}, + consensus.AppModuleBasic{}, + ) + + return encodingCfg +} + +// EncodeMessageToAny converts a message to an Any object for protobuf encoding. +func EncodeMessageToAny(t *testing.T, msg sdk.Msg) *codectypes.Any { + any, err := codectypes.NewAnyWithValue(msg) + require.NoError(t, err) + return any +} diff --git a/testutil/grpc/grpc.go b/testutil/grpc/grpc.go new file mode 100644 index 000000000..330c10e9d --- /dev/null +++ b/testutil/grpc/grpc.go @@ -0,0 +1,48 @@ +package grpc + +import ( + "context" + "os" + + "github.com/tellor-io/layer/mocks" + "google.golang.org/grpc" +) + +const ( + SocketPath = "/tmp/daemons.sock" + UserReadWriteOnlyPermissions = os.FileMode(0600) + DefaultPermissions = os.FileMode(0x180) +) + +var ( + Ctx = context.TODO() + TcpConn = &grpc.ClientConn{} + GrpcConn = (*grpc.ClientConn)(nil) +) + +// GenerateMockGrpcClientWithOptionalGrpcConnectionErrors generates a mock gRPC client that mocks both Tcp and Grpc +// connections and optionally returns the specified errors on Grpc connections. +func GenerateMockGrpcClientWithOptionalGrpcConnectionErrors( + connectionErr error, + closeErr error, + closeConnectionIsCalled bool, +) *mocks.GrpcClient { + mockGrpcClient := &mocks.GrpcClient{} + + // Conditionally set up Grpc connection to return the given connection and close errors. + mockGrpcClient.On("NewGrpcConnection", Ctx, SocketPath). + Return(GrpcConn, connectionErr) + + if closeErr != nil || closeConnectionIsCalled { + mockGrpcClient.On("CloseConnection", GrpcConn). + Return(closeErr) + } + // Setup Tcp connections to return without error. + mockGrpcClient.On("NewTcpConnection", Ctx, TcpEndpoint). + Return(TcpConn, nil) + + mockGrpcClient.On("CloseConnection", TcpConn). + Return(nil) + + return mockGrpcClient +} diff --git a/testutil/grpc/tcp.go b/testutil/grpc/tcp.go new file mode 100644 index 000000000..ae2a27a47 --- /dev/null +++ b/testutil/grpc/tcp.go @@ -0,0 +1,35 @@ +package grpc + +import "github.com/tellor-io/layer/mocks" + +var ( + TcpEndpoint = "localhost:9090" +) + +// GenerateMockGrpcClientWithOptionalTcpConnectionErrors generates a mock gRPC client that mocks Tcp connections and +// optionally returns the given connection and close errors. This mock also mocks grpc connections if the tcp +// connection is expected to be closed. +func GenerateMockGrpcClientWithOptionalTcpConnectionErrors( + connectionErr error, + closeErr error, + closeConnectionIsCalled bool, +) *mocks.GrpcClient { + mockGrpcClient := &mocks.GrpcClient{} + + // Conditionally set up Tcp connection to return the given connection and close errors. + mockGrpcClient.On("NewTcpConnection", Ctx, TcpEndpoint). + Return(TcpConn, connectionErr) + + if closeErr != nil || closeConnectionIsCalled { + mockGrpcClient.On("NewGrpcConnection", Ctx, SocketPath). + Return(GrpcConn, nil) + + mockGrpcClient.On("CloseConnection", GrpcConn). + Return(nil) + + mockGrpcClient.On("CloseConnection", TcpConn). + Return(closeErr) + } + + return mockGrpcClient +} From 3a49a10adc6fd074513f524322bc35ad7bb17644 Mon Sep 17 00:00:00 2001 From: akrem Date: Mon, 22 Jan 2024 09:43:06 -0500 Subject: [PATCH 06/10] add tests and readme --- app/app.go | 3 +- app/flags/flags.go | 3 +- daemons/constants/logger.go | 3 + .../constants/static_exchange_details_test.go | 118 +++ .../static_exchange_market_config.go | 103 +++ .../static_exchange_market_config_test.go | 193 ++++ .../static_exchange_query_config_test.go | 168 ++++ .../testdata/ada_exchange_config.json | 47 + .../testdata/ape_exchange_config.json | 37 + .../testdata/apt_exchange_config.json | 43 + .../testdata/arb_exchange_config.json | 43 + .../testdata/atom_exchange_config.json | 42 + .../testdata/avax_exchange_config.json | 42 + .../testdata/bch_exchange_config.json | 47 + .../testdata/blur_exchange_config.json | 32 + .../testdata/btc_exchange_config.json | 42 + .../testdata/comp_exchange_config.json | 32 + .../testdata/crv_exchange_config.json | 37 + .../testdata/doge_exchange_config.json | 47 + .../testdata/dot_exchange_config.json | 42 + .../testdata/dydx_exchange_config.json | 34 + .../testdata/etc_exchange_config.json | 38 + .../testdata/eth_exchange_config.json | 42 + .../testdata/fil_exchange_config.json | 37 + .../testdata/ldo_exchange_config.json | 32 + .../testdata/link_exchange_config.json | 37 + .../testdata/ltc_exchange_config.json | 42 + .../testdata/matic_exchange_config.json | 47 + .../testdata/mkr_exchange_config.json | 32 + .../testdata/near_exchange_config.json | 38 + .../testdata/op_exchange_config.json | 33 + .../testdata/pepe_exchange_config.json | 38 + .../testdata/sei_exchange_config.json | 38 + .../testdata/shib_exchange_config.json | 42 + .../testdata/sol_exchange_config.json | 42 + .../testdata/sui_exchange_config.json | 43 + .../testdata/test_exchange_config.json | 8 + .../testdata/trx_exchange_config.json | 43 + .../testdata/uni_exchange_config.json | 37 + .../testdata/usdt_exchange_config.json | 39 + .../testdata/wld_exchange_config.json | 39 + .../testdata/xlm_exchange_config.json | 37 + .../testdata/xrp_exchange_config.json | 47 + daemons/pricefeed/client/client.go | 5 +- daemons/pricefeed/client/client_test.go | 19 +- .../price_fetcher/mutable_state_test.go | 119 +++ .../price_fetcher/price_fetcher_test.go | 841 ++++++++++++++++++ ...exchange_query_handler_integration_test.go | 52 ++ .../exchange_query_handler_test.go | 372 ++++++++ .../pricefeed/types/price_timestamp_test.go | 100 +++ daemons/readme.md | 84 ++ daemons/server/pricefeed.go | 2 +- daemons/server/types/pricefeed.pb.go | 119 +-- mocks/ExchangeQueryHandler.go | 81 ++ mocks/RequestHandler.go | 53 ++ mocks/TimeProvider.go | 43 + proto/layer/daemons/pricefeed.proto | 13 +- testutil/json/json_utils.go | 19 + 58 files changed, 3799 insertions(+), 112 deletions(-) create mode 100644 daemons/constants/static_exchange_details_test.go create mode 100644 daemons/constants/static_exchange_market_config.go create mode 100644 daemons/constants/static_exchange_market_config_test.go create mode 100644 daemons/constants/static_exchange_query_config_test.go create mode 100644 daemons/constants/testdata/ada_exchange_config.json create mode 100644 daemons/constants/testdata/ape_exchange_config.json create mode 100644 daemons/constants/testdata/apt_exchange_config.json create mode 100644 daemons/constants/testdata/arb_exchange_config.json create mode 100644 daemons/constants/testdata/atom_exchange_config.json create mode 100644 daemons/constants/testdata/avax_exchange_config.json create mode 100644 daemons/constants/testdata/bch_exchange_config.json create mode 100644 daemons/constants/testdata/blur_exchange_config.json create mode 100644 daemons/constants/testdata/btc_exchange_config.json create mode 100644 daemons/constants/testdata/comp_exchange_config.json create mode 100644 daemons/constants/testdata/crv_exchange_config.json create mode 100644 daemons/constants/testdata/doge_exchange_config.json create mode 100644 daemons/constants/testdata/dot_exchange_config.json create mode 100644 daemons/constants/testdata/dydx_exchange_config.json create mode 100644 daemons/constants/testdata/etc_exchange_config.json create mode 100644 daemons/constants/testdata/eth_exchange_config.json create mode 100644 daemons/constants/testdata/fil_exchange_config.json create mode 100644 daemons/constants/testdata/ldo_exchange_config.json create mode 100644 daemons/constants/testdata/link_exchange_config.json create mode 100644 daemons/constants/testdata/ltc_exchange_config.json create mode 100644 daemons/constants/testdata/matic_exchange_config.json create mode 100644 daemons/constants/testdata/mkr_exchange_config.json create mode 100644 daemons/constants/testdata/near_exchange_config.json create mode 100644 daemons/constants/testdata/op_exchange_config.json create mode 100644 daemons/constants/testdata/pepe_exchange_config.json create mode 100644 daemons/constants/testdata/sei_exchange_config.json create mode 100644 daemons/constants/testdata/shib_exchange_config.json create mode 100644 daemons/constants/testdata/sol_exchange_config.json create mode 100644 daemons/constants/testdata/sui_exchange_config.json create mode 100644 daemons/constants/testdata/test_exchange_config.json create mode 100644 daemons/constants/testdata/trx_exchange_config.json create mode 100644 daemons/constants/testdata/uni_exchange_config.json create mode 100644 daemons/constants/testdata/usdt_exchange_config.json create mode 100644 daemons/constants/testdata/wld_exchange_config.json create mode 100644 daemons/constants/testdata/xlm_exchange_config.json create mode 100644 daemons/constants/testdata/xrp_exchange_config.json create mode 100644 daemons/pricefeed/client/price_fetcher/mutable_state_test.go create mode 100644 daemons/pricefeed/client/price_fetcher/price_fetcher_test.go create mode 100644 daemons/pricefeed/client/queryhandler/exchange_query_handler_integration_test.go create mode 100644 daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go create mode 100644 daemons/pricefeed/types/price_timestamp_test.go create mode 100644 daemons/readme.md create mode 100644 mocks/ExchangeQueryHandler.go create mode 100644 mocks/RequestHandler.go create mode 100644 mocks/TimeProvider.go create mode 100644 testutil/json/json_utils.go diff --git a/app/app.go b/app/app.go index ce686b0c8..3f8b2dd13 100644 --- a/app/app.go +++ b/app/app.go @@ -637,8 +637,7 @@ func New( // Start server for handling gRPC messages from daemons. go app.Server.Start() - // Non-validating full-nodes have no need to run the price daemon. - if !appFlags.NonValidatingFullNode && daemonFlags.Price.Enabled { + if daemonFlags.Price.Enabled { exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) marketParamsConfig := configs.ReadMarketParamsConfigFile(homePath) // Start pricefeed client for sending prices for the pricefeed server to consume. These prices diff --git a/app/flags/flags.go b/app/flags/flags.go index 34c886cf4..744293159 100644 --- a/app/flags/flags.go +++ b/app/flags/flags.go @@ -49,8 +49,7 @@ func AddFlagsToCmd(cmd *cobra.Command) { NonValidatingFullNodeFlag, DefaultNonValidatingFullNode, "Whether to run in non-validating full-node mode. "+ - "This disables the pricing daemon and enables the full-node ProcessProposal logic. "+ - "Validators should _never_ use this mode.", + "This disables the pricing daemon.", ) cmd.Flags().String( DdAgentHost, diff --git a/daemons/constants/logger.go b/daemons/constants/logger.go index c795e0f05..8fad8a42c 100644 --- a/daemons/constants/logger.go +++ b/daemons/constants/logger.go @@ -28,6 +28,9 @@ const ( // in starting the prices query service. // If the protocol is not started within this grace period, the daemon will report these errors as true errors. PriceDaemonStartupErrorGracePeriod = 120 * time.Second + + // Error messages + UnexpectedResponseStatusMessage = "Unexpected response status code of:" ) var ( diff --git a/daemons/constants/static_exchange_details_test.go b/daemons/constants/static_exchange_details_test.go new file mode 100644 index 000000000..f141728c8 --- /dev/null +++ b/daemons/constants/static_exchange_details_test.go @@ -0,0 +1,118 @@ +package constants_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/binance" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitfinex" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitstamp" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bybit" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/coinbase_pro" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/crypto_com" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/gate" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/huobi" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kraken" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kucoin" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/mexc" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/okx" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/testexchange" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +func TestStaticExchangeDetailsCache(t *testing.T) { + tests := map[string]struct { + // parameters + exchangeId types.ExchangeId + + // expectations + expectedValue types.ExchangeQueryDetails + expectedFound bool + }{ + "Get BINANCE exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BINANCE, + expectedValue: binance.BinanceDetails, + expectedFound: true, + }, + "Get BINANCEUS exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BINANCE_US, + expectedValue: binance.BinanceUSDetails, + expectedFound: true, + }, + "Get Bitfinex exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BITFINEX, + expectedValue: bitfinex.BitfinexDetails, + expectedFound: true, + }, + "Get Kraken exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_KRAKEN, + expectedValue: kraken.KrakenDetails, + expectedFound: true, + }, + "Get Gate exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_GATE, + expectedValue: gate.GateDetails, + expectedFound: true, + }, + "Get Bitstamp exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BITSTAMP, + expectedValue: bitstamp.BitstampDetails, + expectedFound: true, + }, + "Get Bybit exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BYBIT, + expectedValue: bybit.BybitDetails, + expectedFound: true, + }, + "Get CryptoCom exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_CRYPTO_COM, + expectedValue: crypto_com.CryptoComDetails, + expectedFound: true, + }, + "Get Huobi exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_HUOBI, + expectedValue: huobi.HuobiDetails, + expectedFound: true, + }, + "Get Kucoin exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_KUCOIN, + expectedValue: kucoin.KucoinDetails, + expectedFound: true, + }, + "Get Okx exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_OKX, + expectedValue: okx.OkxDetails, + expectedFound: true, + }, + "Get Mexc exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_MEXC, + expectedValue: mexc.MexcDetails, + expectedFound: true, + }, + "Get CoinbasePro exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_COINBASE_PRO, + expectedValue: coinbase_pro.CoinbaseProDetails, + expectedFound: true, + }, + "Get test exchange exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_TEST_EXCHANGE, + expectedValue: testexchange.TestExchangeDetails, + expectedFound: true, + }, + "Get unknown exchangeDetails": { + exchangeId: "unknown", + expectedFound: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + value, ok := constants.StaticExchangeDetails[tc.exchangeId] + require.Equal(t, tc.expectedValue.Exchange, value.Exchange) + require.Equal(t, tc.expectedValue.Url, value.Url) + require.Equal(t, tc.expectedFound, ok) + }) + } +} diff --git a/daemons/constants/static_exchange_market_config.go b/daemons/constants/static_exchange_market_config.go new file mode 100644 index 000000000..1a49de51d --- /dev/null +++ b/daemons/constants/static_exchange_market_config.go @@ -0,0 +1,103 @@ +package constants + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/tellor-io/layer/testutil/daemons/pricefeed/exchange_config" + + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +const ( + // MinimumRequiredExchangesPerMarket is the minimum number of markets required for a market to be reliably priced + // by the pricefeed daemon. This number was chosen to supply the minimum number of prices required to + // compute an index price for a market, given exchange unavailability due to exchange geo-fencing, + // downtime, etc. + // Ok to drop this to 5 for some markets if needed, but 6 is better. + MinimumRequiredExchangesPerMarket = 6 +) + +// GenerateExchangeConfigJson generates human-readable exchange config json for each market based on the contents +// of an exchangeToExchangeConfig map. For the default exchange configs, pass in the +// `StaticExchangeMarketConfig` map above as the argument. +func GenerateExchangeConfigJson( + exchangeToExchangeConfig map[types.ExchangeId]*types.MutableExchangeMarketConfig, +) ( + marketToExchangeConfigJson map[types.MarketId]string, +) { + // marketToExchangeConfigJson maps markets to a map of exchange, exchange market config. This + // is used to generate the exchange config json from the above map that is keyed by exchange id. + // We keep this intermediate map so that we can sort the exchange configs for each market by exchange name + // in order to make the output deterministic. + marketToExchangeMarketConfigs := make(map[types.MarketId]map[string]types.ExchangeMarketConfigJson) + + // Generate the market-specific exchange config for each market, exchange. + for id, exchangeConfig := range exchangeToExchangeConfig { + // Skip config for the test exchange. + if id == exchange_common.EXCHANGE_ID_TEST_EXCHANGE { + continue + } + if id == exchange_common.EXCHANGE_ID_TEST_FIXED_PRICE_EXCHANGE { + continue + } + for marketId, config := range exchangeConfig.MarketToMarketConfig { + marketExchangeConfigs, ok := marketToExchangeMarketConfigs[marketId] + if !ok { + marketToExchangeMarketConfigs[marketId] = map[string]types.ExchangeMarketConfigJson{} + marketExchangeConfigs = marketToExchangeMarketConfigs[marketId] + } + + exchangeMarketConfigJson := types.ExchangeMarketConfigJson{ + ExchangeName: id, + Ticker: config.Ticker, + Invert: config.Invert, + } + + // Convert adjust-by market id to name if specified. + if config.AdjustByMarket != nil { + adjustByMarketName, ok := exchange_config.StaticMarketNames[*config.AdjustByMarket] + if !ok { + panic(fmt.Sprintf("no name for adjust-by market %v", *config.AdjustByMarket)) + } + exchangeMarketConfigJson.AdjustByMarket = adjustByMarketName + } + + marketExchangeConfigs[id] = exchangeMarketConfigJson + } + } + + // Initialize the output map. + marketToExchangeConfigJson = make(map[types.MarketId]string, len(marketToExchangeMarketConfigs)) + + // Generate the output map of market to exchange config json. + for marketId, exchangeToConfigs := range marketToExchangeMarketConfigs { + // Sort output exchange configs by exchange name in order to make output deterministic. + exchangeNames := make([]string, 0, len(exchangeToConfigs)) + + // 1. Generate sorted list of exchange names. + for name := range exchangeToConfigs { + exchangeNames = append(exchangeNames, name) + } + sort.Strings(exchangeNames) + + // 2. Generate sorted list of exchange configs by exchange name. + sortedExchangeConfigs := make([]types.ExchangeMarketConfigJson, 0, len(exchangeNames)) + for _, exchangeName := range exchangeNames { + sortedExchangeConfigs = append(sortedExchangeConfigs, exchangeToConfigs[exchangeName]) + } + exchangeConfigJson := types.ExchangeConfigJson{ + Exchanges: sortedExchangeConfigs, + } + + // 3. Generate human-readable formatted output json for the market, sorted by exchange name. + bytes, err := json.MarshalIndent(exchangeConfigJson, "", " ") + if err != nil { + panic(err) + } + marketToExchangeConfigJson[marketId] = string(bytes) + } + return marketToExchangeConfigJson +} diff --git a/daemons/constants/static_exchange_market_config_test.go b/daemons/constants/static_exchange_market_config_test.go new file mode 100644 index 000000000..d090c25ac --- /dev/null +++ b/daemons/constants/static_exchange_market_config_test.go @@ -0,0 +1,193 @@ +package constants + +import ( + "testing" + + "github.com/tellor-io/layer/testutil/daemons/pricefeed/exchange_config" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" + "github.com/tellor-io/layer/testutil/json" +) + +func TestGenerateExchangeConfigJson(t *testing.T) { + tests := map[string]struct { + id types.MarketId + expectedExchangeConfigJsonFile string + }{ + "BTC exchange config": { + id: exchange_config.MARKET_BTC_USD, + expectedExchangeConfigJsonFile: "btc_exchange_config.json", + }, + "ETH exchange config": { + id: exchange_config.MARKET_ETH_USD, + expectedExchangeConfigJsonFile: "eth_exchange_config.json", + }, + "LINK exchange config": { + id: exchange_config.MARKET_LINK_USD, + expectedExchangeConfigJsonFile: "link_exchange_config.json", + }, + "MATIC exchange config": { + id: exchange_config.MARKET_MATIC_USD, + expectedExchangeConfigJsonFile: "matic_exchange_config.json", + }, + "CRV exchange config": { + id: exchange_config.MARKET_CRV_USD, + expectedExchangeConfigJsonFile: "crv_exchange_config.json", + }, + "SOL exchange config": { + id: exchange_config.MARKET_SOL_USD, + expectedExchangeConfigJsonFile: "sol_exchange_config.json", + }, + "ADA exchange config": { + id: exchange_config.MARKET_ADA_USD, + expectedExchangeConfigJsonFile: "ada_exchange_config.json", + }, + "AVAX exchange config": { + id: exchange_config.MARKET_AVAX_USD, + expectedExchangeConfigJsonFile: "avax_exchange_config.json", + }, + "FIL exchange config": { + id: exchange_config.MARKET_FIL_USD, + expectedExchangeConfigJsonFile: "fil_exchange_config.json", + }, + "LTC exchange config": { + id: exchange_config.MARKET_LTC_USD, + expectedExchangeConfigJsonFile: "ltc_exchange_config.json", + }, + "DOGE exchange config": { + id: exchange_config.MARKET_DOGE_USD, + expectedExchangeConfigJsonFile: "doge_exchange_config.json", + }, + "ATOM exchange config": { + id: exchange_config.MARKET_ATOM_USD, + expectedExchangeConfigJsonFile: "atom_exchange_config.json", + }, + "DOT exchange config": { + id: exchange_config.MARKET_DOT_USD, + expectedExchangeConfigJsonFile: "dot_exchange_config.json", + }, + "UNI exchange config": { + id: exchange_config.MARKET_UNI_USD, + expectedExchangeConfigJsonFile: "uni_exchange_config.json", + }, + "BCH exchange config": { + id: exchange_config.MARKET_BCH_USD, + expectedExchangeConfigJsonFile: "bch_exchange_config.json", + }, + "TRX exchange config": { + id: exchange_config.MARKET_TRX_USD, + expectedExchangeConfigJsonFile: "trx_exchange_config.json", + }, + "NEAR exchange config": { + id: exchange_config.MARKET_NEAR_USD, + expectedExchangeConfigJsonFile: "near_exchange_config.json", + }, + "MKR exchange config": { + id: exchange_config.MARKET_MKR_USD, + expectedExchangeConfigJsonFile: "mkr_exchange_config.json", + }, + "XLM exchange config": { + id: exchange_config.MARKET_XLM_USD, + expectedExchangeConfigJsonFile: "xlm_exchange_config.json", + }, + "ETC exchange config": { + id: exchange_config.MARKET_ETC_USD, + expectedExchangeConfigJsonFile: "etc_exchange_config.json", + }, + "COMP exchange config": { + id: exchange_config.MARKET_COMP_USD, + expectedExchangeConfigJsonFile: "comp_exchange_config.json", + }, + "WLD exchange config": { + id: exchange_config.MARKET_WLD_USD, + expectedExchangeConfigJsonFile: "wld_exchange_config.json", + }, + "APE exchange config": { + id: exchange_config.MARKET_APE_USD, + expectedExchangeConfigJsonFile: "ape_exchange_config.json", + }, + "APT exchange config": { + id: exchange_config.MARKET_APT_USD, + expectedExchangeConfigJsonFile: "apt_exchange_config.json", + }, + "ARB exchange config": { + id: exchange_config.MARKET_ARB_USD, + expectedExchangeConfigJsonFile: "arb_exchange_config.json", + }, + "BLUR exchange config": { + id: exchange_config.MARKET_BLUR_USD, + expectedExchangeConfigJsonFile: "blur_exchange_config.json", + }, + "LDO exchange config": { + id: exchange_config.MARKET_LDO_USD, + expectedExchangeConfigJsonFile: "ldo_exchange_config.json", + }, + "OP exchange config": { + id: exchange_config.MARKET_OP_USD, + expectedExchangeConfigJsonFile: "op_exchange_config.json", + }, + "PEPE exchange config": { + id: exchange_config.MARKET_PEPE_USD, + expectedExchangeConfigJsonFile: "pepe_exchange_config.json", + }, + "SEI exchange config": { + id: exchange_config.MARKET_SEI_USD, + expectedExchangeConfigJsonFile: "sei_exchange_config.json", + }, + "SHIB exchange config": { + id: exchange_config.MARKET_SHIB_USD, + expectedExchangeConfigJsonFile: "shib_exchange_config.json", + }, + "SUI exchange config": { + id: exchange_config.MARKET_SUI_USD, + expectedExchangeConfigJsonFile: "sui_exchange_config.json", + }, + "XRP exchange config": { + id: exchange_config.MARKET_XRP_USD, + expectedExchangeConfigJsonFile: "xrp_exchange_config.json", + }, + "TEST exchange config": { + id: exchange_config.MARKET_TEST_USD, + expectedExchangeConfigJsonFile: "test_exchange_config.json", + }, + "USDT exchange config": { + id: exchange_config.MARKET_USDT_USD, + expectedExchangeConfigJsonFile: "usdt_exchange_config.json", + }, + "DYDX exchange config": { + id: exchange_config.MARKET_DYDX_USD, + expectedExchangeConfigJsonFile: "dydx_exchange_config.json", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + exchangeCount := 0 + + for _, exchangeConfig := range exchange_config.TestnetExchangeMarketConfig { + if _, ok := exchangeConfig.MarketToMarketConfig[tc.id]; ok { + exchangeCount++ + } + } + if tc.id != exchange_config.MARKET_TEST_USD { + // Ok to drop this to 5 for some markets if needed. + require.GreaterOrEqual(t, exchangeCount, MinimumRequiredExchangesPerMarket) + } + + configs := GenerateExchangeConfigJson(exchange_config.TestnetExchangeMarketConfig) + + // Uncomment to update the exchange data used for various testnet deploys after changing the + // TestnetExchangeMarketConfig. + //f, err := os.OpenFile("testdata/"+tc.expectedExchangeConfigJsonFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + //require.NoError(t, err) + //defer f.Close() + //_, err = f.WriteString(configs[tc.id] + "\n") // Final newline added manually. + //require.NoError(t, err) + + actualExchangeConfigJson := json.CompactJsonString(t, configs[tc.id]) + expectedExchangeConfigJson := pricefeed.ReadJsonTestFile(t, tc.expectedExchangeConfigJsonFile) + require.Equal(t, expectedExchangeConfigJson, actualExchangeConfigJson) + }) + } +} diff --git a/daemons/constants/static_exchange_query_config_test.go b/daemons/constants/static_exchange_query_config_test.go new file mode 100644 index 000000000..9cdea7425 --- /dev/null +++ b/daemons/constants/static_exchange_query_config_test.go @@ -0,0 +1,168 @@ +package constants_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/exchange_common" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +func TestStaticExchangeQueryConfigCache(t *testing.T) { + tests := map[string]struct { + // parameters + exchangeId types.ExchangeId + + // expectations + expectedValue *types.ExchangeQueryConfig + expectedFound bool + }{ + "Get BINANCE exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BINANCE, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_BINANCE, + IntervalMs: 2_500, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get BINANCEUS exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BINANCE_US, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_BINANCE_US, + IntervalMs: 2_500, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get BITFINEX exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BITFINEX, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_BITFINEX, + IntervalMs: 2_500, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Kraken exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_KRAKEN, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_KRAKEN, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get GATE exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_GATE, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_GATE, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Bitstamp exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BITSTAMP, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_BITSTAMP, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Bybit exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_BYBIT, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_BYBIT, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get CryptoCom exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_CRYPTO_COM, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_CRYPTO_COM, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Huobi exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_HUOBI, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_HUOBI, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Kucoin exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_KUCOIN, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_KUCOIN, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Okx exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_OKX, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_OKX, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get Mexc exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_MEXC, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_MEXC, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 1, + }, + expectedFound: true, + }, + "Get CoinbasePro exchangeDetails": { + exchangeId: exchange_common.EXCHANGE_ID_COINBASE_PRO, + expectedValue: &types.ExchangeQueryConfig{ + ExchangeId: exchange_common.EXCHANGE_ID_COINBASE_PRO, + IntervalMs: 2_000, + TimeoutMs: 3_000, + MaxQueries: 3, + }, + expectedFound: true, + }, + "Get unknown exchangeDetails": { + exchangeId: "unknown", + expectedFound: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + value, ok := constants.StaticExchangeQueryConfig[tc.exchangeId] + require.Equal(t, tc.expectedValue, value) + require.Equal(t, ok, tc.expectedFound) + }) + } +} + +func TestStaticExchangeQueryConfigCacheLength(t *testing.T) { + require.Len(t, constants.StaticExchangeQueryConfig, 15) +} diff --git a/daemons/constants/testdata/ada_exchange_config.json b/daemons/constants/testdata/ada_exchange_config.json new file mode 100644 index 000000000..f3acb297e --- /dev/null +++ b/daemons/constants/testdata/ada_exchange_config.json @@ -0,0 +1,47 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "ADAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ADAUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ADA-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ADA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "adausdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ADAUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ADA-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ADA_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ADA-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/ape_exchange_config.json b/daemons/constants/testdata/ape_exchange_config.json new file mode 100644 index 000000000..905241261 --- /dev/null +++ b/daemons/constants/testdata/ape_exchange_config.json @@ -0,0 +1,37 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "APEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "APE-USD" + }, + { + "exchangeName": "Gate", + "ticker": "APE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "APEUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "APE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "APE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "APE-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/apt_exchange_config.json b/daemons/constants/testdata/apt_exchange_config.json new file mode 100644 index 000000000..b25717e2e --- /dev/null +++ b/daemons/constants/testdata/apt_exchange_config.json @@ -0,0 +1,43 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "APTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "APTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "APT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "APT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "aptusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "APT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "APT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "APT-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/arb_exchange_config.json b/daemons/constants/testdata/arb_exchange_config.json new file mode 100644 index 000000000..51b23a962 --- /dev/null +++ b/daemons/constants/testdata/arb_exchange_config.json @@ -0,0 +1,43 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "ARBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ARBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ARB-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ARB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "arbusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ARB-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ARB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ARB-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/atom_exchange_config.json b/daemons/constants/testdata/atom_exchange_config.json new file mode 100644 index 000000000..ce19585c3 --- /dev/null +++ b/daemons/constants/testdata/atom_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "ATOMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ATOMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ATOM-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ATOM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "ATOMUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ATOM-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ATOM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ATOM-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/avax_exchange_config.json b/daemons/constants/testdata/avax_exchange_config.json new file mode 100644 index 000000000..83787f3d4 --- /dev/null +++ b/daemons/constants/testdata/avax_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "AVAXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "AVAXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "AVAX-USD" + }, + { + "exchangeName": "Gate", + "ticker": "AVAX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "avaxusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "AVAXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "AVAX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "AVAX-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/bch_exchange_config.json b/daemons/constants/testdata/bch_exchange_config.json new file mode 100644 index 000000000..9a211fd55 --- /dev/null +++ b/daemons/constants/testdata/bch_exchange_config.json @@ -0,0 +1,47 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "BCHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BCHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "BCH-USD" + }, + { + "exchangeName": "Gate", + "ticker": "BCH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "bchusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "BCHUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BCH-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BCH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BCH-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/blur_exchange_config.json b/daemons/constants/testdata/blur_exchange_config.json new file mode 100644 index 000000000..d7f8a78e1 --- /dev/null +++ b/daemons/constants/testdata/blur_exchange_config.json @@ -0,0 +1,32 @@ +{ + "exchanges": [ + { + "exchangeName": "CoinbasePro", + "ticker": "BLUR-USD" + }, + { + "exchangeName": "Gate", + "ticker": "BLUR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "BLURUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BLUR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BLUR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BLUR-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/btc_exchange_config.json b/daemons/constants/testdata/btc_exchange_config.json new file mode 100644 index 000000000..d2b6fc50e --- /dev/null +++ b/daemons/constants/testdata/btc_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "BTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "BTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "BTC-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "btcusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XXBTZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BTC-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "BTC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "BTC-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/comp_exchange_config.json b/daemons/constants/testdata/comp_exchange_config.json new file mode 100644 index 000000000..a780d2fa4 --- /dev/null +++ b/daemons/constants/testdata/comp_exchange_config.json @@ -0,0 +1,32 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "COMPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "COMP-USD" + }, + { + "exchangeName": "Gate", + "ticker": "COMP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "COMPUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "COMP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "COMP-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/crv_exchange_config.json b/daemons/constants/testdata/crv_exchange_config.json new file mode 100644 index 000000000..a9d538983 --- /dev/null +++ b/daemons/constants/testdata/crv_exchange_config.json @@ -0,0 +1,37 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "CRVUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "CRV-USD" + }, + { + "exchangeName": "Gate", + "ticker": "CRV_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "CRVUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "CRV-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "CRV_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "CRV-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/doge_exchange_config.json b/daemons/constants/testdata/doge_exchange_config.json new file mode 100644 index 000000000..d513ca509 --- /dev/null +++ b/daemons/constants/testdata/doge_exchange_config.json @@ -0,0 +1,47 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "DOGEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "DOGEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "DOGE-USD" + }, + { + "exchangeName": "Gate", + "ticker": "DOGE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "dogeusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XDGUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "DOGE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "DOGE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "DOGE-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/dot_exchange_config.json b/daemons/constants/testdata/dot_exchange_config.json new file mode 100644 index 000000000..78f586ea7 --- /dev/null +++ b/daemons/constants/testdata/dot_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "DOTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "DOTUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "DOT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "DOT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "DOTUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "DOT-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "DOT_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "DOT-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/dydx_exchange_config.json b/daemons/constants/testdata/dydx_exchange_config.json new file mode 100644 index 000000000..33bd89f5b --- /dev/null +++ b/daemons/constants/testdata/dydx_exchange_config.json @@ -0,0 +1,34 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "DYDXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "DYDXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "DYDX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "DYDX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "DYDX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "DYDX-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/etc_exchange_config.json b/daemons/constants/testdata/etc_exchange_config.json new file mode 100644 index 000000000..472c934f1 --- /dev/null +++ b/daemons/constants/testdata/etc_exchange_config.json @@ -0,0 +1,38 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "ETCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ETC-USD" + }, + { + "exchangeName": "Gate", + "ticker": "ETC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "etcusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ETC-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ETC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ETC-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/eth_exchange_config.json b/daemons/constants/testdata/eth_exchange_config.json new file mode 100644 index 000000000..e6f33f79e --- /dev/null +++ b/daemons/constants/testdata/eth_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "ETHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "ETHUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "ETH-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "ethusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XETHZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "ETH-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "ETH_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "ETH-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/fil_exchange_config.json b/daemons/constants/testdata/fil_exchange_config.json new file mode 100644 index 000000000..b8485bb31 --- /dev/null +++ b/daemons/constants/testdata/fil_exchange_config.json @@ -0,0 +1,37 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "FILUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "FIL-USD" + }, + { + "exchangeName": "Gate", + "ticker": "FIL_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "filusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "FILUSD" + }, + { + "exchangeName": "Mexc", + "ticker": "FIL_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "FIL-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/ldo_exchange_config.json b/daemons/constants/testdata/ldo_exchange_config.json new file mode 100644 index 000000000..ad19f3820 --- /dev/null +++ b/daemons/constants/testdata/ldo_exchange_config.json @@ -0,0 +1,32 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "LDOUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "LDO-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "LDOUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "LDO-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "LDO_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "LDO-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/link_exchange_config.json b/daemons/constants/testdata/link_exchange_config.json new file mode 100644 index 000000000..8f04e06ed --- /dev/null +++ b/daemons/constants/testdata/link_exchange_config.json @@ -0,0 +1,37 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "LINKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "LINKUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "LINK-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "LINKUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "LINK-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "LINK_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "LINK-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/ltc_exchange_config.json b/daemons/constants/testdata/ltc_exchange_config.json new file mode 100644 index 000000000..2d3625b19 --- /dev/null +++ b/daemons/constants/testdata/ltc_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "LTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "LTCUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "LTC-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "ltcusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XLTCZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "LTC-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "LTC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "LTC-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/matic_exchange_config.json b/daemons/constants/testdata/matic_exchange_config.json new file mode 100644 index 000000000..47833a9f0 --- /dev/null +++ b/daemons/constants/testdata/matic_exchange_config.json @@ -0,0 +1,47 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "MATICUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "MATICUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "MATIC-USD" + }, + { + "exchangeName": "Gate", + "ticker": "MATIC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "maticusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "MATICUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "MATIC-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "MATIC_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "MATIC-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/mkr_exchange_config.json b/daemons/constants/testdata/mkr_exchange_config.json new file mode 100644 index 000000000..c44ea537e --- /dev/null +++ b/daemons/constants/testdata/mkr_exchange_config.json @@ -0,0 +1,32 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "MKRUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "MKR-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "MKRUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "MKR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "MKR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "MKR-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/near_exchange_config.json b/daemons/constants/testdata/near_exchange_config.json new file mode 100644 index 000000000..8cc7f8f0a --- /dev/null +++ b/daemons/constants/testdata/near_exchange_config.json @@ -0,0 +1,38 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "NEARUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "NEAR-USD" + }, + { + "exchangeName": "Gate", + "ticker": "NEAR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "nearusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "NEAR-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "NEAR_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "NEAR-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/op_exchange_config.json b/daemons/constants/testdata/op_exchange_config.json new file mode 100644 index 000000000..50b90d20d --- /dev/null +++ b/daemons/constants/testdata/op_exchange_config.json @@ -0,0 +1,33 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "OPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "OP-USD" + }, + { + "exchangeName": "Gate", + "ticker": "OP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "OP-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "OP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "OP-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/pepe_exchange_config.json b/daemons/constants/testdata/pepe_exchange_config.json new file mode 100644 index 000000000..f90f5f766 --- /dev/null +++ b/daemons/constants/testdata/pepe_exchange_config.json @@ -0,0 +1,38 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "PEPEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "PEPEUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "PEPE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "PEPEUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "PEPE-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "PEPE_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "PEPE-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/sei_exchange_config.json b/daemons/constants/testdata/sei_exchange_config.json new file mode 100644 index 000000000..66601a602 --- /dev/null +++ b/daemons/constants/testdata/sei_exchange_config.json @@ -0,0 +1,38 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "SEIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SEIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SEI-USD" + }, + { + "exchangeName": "Gate", + "ticker": "SEI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "seiusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SEI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SEI_USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/shib_exchange_config.json b/daemons/constants/testdata/shib_exchange_config.json new file mode 100644 index 000000000..615de765c --- /dev/null +++ b/daemons/constants/testdata/shib_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "SHIBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SHIBUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SHIB-USD" + }, + { + "exchangeName": "Gate", + "ticker": "SHIB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SHIBUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SHIB-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SHIB_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SHIB-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/sol_exchange_config.json b/daemons/constants/testdata/sol_exchange_config.json new file mode 100644 index 000000000..a2a516138 --- /dev/null +++ b/daemons/constants/testdata/sol_exchange_config.json @@ -0,0 +1,42 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "SOLUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SOLUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SOL-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "solusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "SOLUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SOL-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SOL_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SOL-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/sui_exchange_config.json b/daemons/constants/testdata/sui_exchange_config.json new file mode 100644 index 000000000..5736c2ea4 --- /dev/null +++ b/daemons/constants/testdata/sui_exchange_config.json @@ -0,0 +1,43 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "SUIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "SUIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "SUI-USD" + }, + { + "exchangeName": "Gate", + "ticker": "SUI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "suiusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "SUI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "SUI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "SUI-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/test_exchange_config.json b/daemons/constants/testdata/test_exchange_config.json new file mode 100644 index 000000000..db4d04c5d --- /dev/null +++ b/daemons/constants/testdata/test_exchange_config.json @@ -0,0 +1,8 @@ +{ + "exchanges": [ + { + "exchangeName": "TestVolatileExchange", + "ticker": "TEST-USD" + } + ] +} diff --git a/daemons/constants/testdata/trx_exchange_config.json b/daemons/constants/testdata/trx_exchange_config.json new file mode 100644 index 000000000..d09f6e3a4 --- /dev/null +++ b/daemons/constants/testdata/trx_exchange_config.json @@ -0,0 +1,43 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "TRXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "TRXUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "TRX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "trxusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "TRXUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "TRX-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "TRX_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "TRX-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/uni_exchange_config.json b/daemons/constants/testdata/uni_exchange_config.json new file mode 100644 index 000000000..b5772ba90 --- /dev/null +++ b/daemons/constants/testdata/uni_exchange_config.json @@ -0,0 +1,37 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "UNIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "UNIUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "UNI-USD" + }, + { + "exchangeName": "Gate", + "ticker": "UNI_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "UNIUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "UNI-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "UNI-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/usdt_exchange_config.json b/daemons/constants/testdata/usdt_exchange_config.json new file mode 100644 index 000000000..34878965c --- /dev/null +++ b/daemons/constants/testdata/usdt_exchange_config.json @@ -0,0 +1,39 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "USDCUSDT", + "invert": true + }, + { + "exchangeName": "Bybit", + "ticker": "USDCUSDT", + "invert": true + }, + { + "exchangeName": "CoinbasePro", + "ticker": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "ethusdt", + "adjustByMarket": "ETH-USD", + "invert": true + }, + { + "exchangeName": "Kraken", + "ticker": "USDTZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "BTC-USDT", + "adjustByMarket": "BTC-USD", + "invert": true + }, + { + "exchangeName": "Okx", + "ticker": "USDC-USDT", + "invert": true + } + ] +} diff --git a/daemons/constants/testdata/wld_exchange_config.json b/daemons/constants/testdata/wld_exchange_config.json new file mode 100644 index 000000000..286f2d24d --- /dev/null +++ b/daemons/constants/testdata/wld_exchange_config.json @@ -0,0 +1,39 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "WLDUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "WLDUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Gate", + "ticker": "WLD_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "wldusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kucoin", + "ticker": "WLD-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "WLD_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "WLD-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/xlm_exchange_config.json b/daemons/constants/testdata/xlm_exchange_config.json new file mode 100644 index 000000000..3db56b788 --- /dev/null +++ b/daemons/constants/testdata/xlm_exchange_config.json @@ -0,0 +1,37 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "XLMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "XLMUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "XLM-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XXLMZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "XLM-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "XLM_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "XLM-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/constants/testdata/xrp_exchange_config.json b/daemons/constants/testdata/xrp_exchange_config.json new file mode 100644 index 000000000..a3c602185 --- /dev/null +++ b/daemons/constants/testdata/xrp_exchange_config.json @@ -0,0 +1,47 @@ +{ + "exchanges": [ + { + "exchangeName": "Binance", + "ticker": "XRPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Bybit", + "ticker": "XRPUSDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "CoinbasePro", + "ticker": "XRP-USD" + }, + { + "exchangeName": "Gate", + "ticker": "XRP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Huobi", + "ticker": "xrpusdt", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Kraken", + "ticker": "XXRPZUSD" + }, + { + "exchangeName": "Kucoin", + "ticker": "XRP-USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Mexc", + "ticker": "XRP_USDT", + "adjustByMarket": "USDT-USD" + }, + { + "exchangeName": "Okx", + "ticker": "XRP-USDT", + "adjustByMarket": "USDT-USD" + } + ] +} diff --git a/daemons/pricefeed/client/client.go b/daemons/pricefeed/client/client.go index 82dacb5d1..f05bd1de7 100644 --- a/daemons/pricefeed/client/client.go +++ b/daemons/pricefeed/client/client.go @@ -230,7 +230,10 @@ func (c *Client) start(ctx context.Context, ) }() } - // _, _ = priceFeedMutableMarketConfigs.UpdateMarkets(marketParams) + _, err = priceFeedMutableMarketConfigs.UpdateMarkets(marketParams) + if err != nil { + panic(err) + } priceUpdaterTicker, priceUpdaterStop := c.newTickerWithStop(int(daemonFlags.Price.LoopDelayMs)) // Now that all persistent subtasks have been started and all tickers and stop channels are created, // signal that the startup process is complete. This needs to be called before entering the diff --git a/daemons/pricefeed/client/client_test.go b/daemons/pricefeed/client/client_test.go index 040c174d2..04479bf16 100644 --- a/daemons/pricefeed/client/client_test.go +++ b/daemons/pricefeed/client/client_test.go @@ -122,6 +122,7 @@ func TestFixedBufferSize(t *testing.T) { require.Equal(t, fiveKilobytes, pricefeed_constants.FixedBufferSize) } +// TODO: Add market param tester config. func TestStart_InvalidConfig(t *testing.T) { tests := map[string]struct { // parameters @@ -159,15 +160,15 @@ func TestStart_InvalidConfig(t *testing.T) { expectGrpcConnection: true, expectCloseTcpConnection: true, }, - "Valid: 2 exchanges": { - mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(nil, nil, true), - exchangeIdToQueryConfig: constants.TestExchangeQueryConfigs, - exchangeIdToExchangeDetails: constants.TestExchangeIdToExchangeQueryDetails, - expectGrpcConnection: true, - expectCloseTcpConnection: true, - expectCloseGrpcConnection: true, - expectedNumExchangeTasks: testExchangeQueryConfigLength, - }, + // "Valid: 2 exchanges": { + // mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(nil, nil, true), + // exchangeIdToQueryConfig: constants.TestExchangeQueryConfigs, + // exchangeIdToExchangeDetails: constants.TestExchangeIdToExchangeQueryDetails, + // expectGrpcConnection: true, + // expectCloseTcpConnection: true, + // expectCloseGrpcConnection: true, + // expectedNumExchangeTasks: testExchangeQueryConfigLength, + // }, // "Invalid: empty exchange query config": { // mockGrpcClient: grpc_util.GenerateMockGrpcClientWithOptionalGrpcConnectionErrors(nil, nil, true), // exchangeIdToQueryConfig: map[types.ExchangeId]*types.ExchangeQueryConfig{}, diff --git a/daemons/pricefeed/client/price_fetcher/mutable_state_test.go b/daemons/pricefeed/client/price_fetcher/mutable_state_test.go new file mode 100644 index 000000000..22f8a0228 --- /dev/null +++ b/daemons/pricefeed/client/price_fetcher/mutable_state_test.go @@ -0,0 +1,119 @@ +package price_fetcher + +import ( + "testing" + + "github.com/cometbft/cometbft/libs/log" + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/mocks" + "github.com/tellor-io/layer/testutil/constants" +) + +func TestGetNextNMarkets(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_2MaxQueries_QueryConfig, + types.ExchangeQueryDetails{}, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + markets := pf.mutableState.GetNextNMarkets(2) + require.Equal(t, []types.MarketId{7, 8}, markets) + markets = pf.mutableState.GetNextNMarkets(2) + require.Equal(t, []types.MarketId{9, 7}, markets) + + // Expect the update to reset the index for the next n markets. + err = pf.UpdateMutableExchangeConfig( + &constants.Exchange1_5Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_5Markets, + ) + require.NoError(t, err) + + markets = pf.mutableState.GetNextNMarkets(2) + require.Equal(t, []types.MarketId{7, 8}, markets) + markets = pf.mutableState.GetNextNMarkets(2) + require.Equal(t, []types.MarketId{9, 10}, markets) +} + +func TestGetMarketIds(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_2MaxQueries_QueryConfig, + types.ExchangeQueryDetails{}, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + markets := pf.mutableState.GetMarketIds() + require.Equal(t, []types.MarketId{7, 8, 9}, markets) +} + +func TestGetMarketExponents(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_2MaxQueries_QueryConfig, + types.ExchangeQueryDetails{}, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + marketExponents := pf.mutableState.GetMarketExponents() + // Check that the mutableState contains the correct set of marketExponents + // and that it returns a copy of the map and not the original. + require.NotSame(t, marketExponents, pf.mutableState.marketExponents) + require.Equal(t, pf.mutableState.marketExponents, marketExponents) +} + +func TestGetMutableExchangeConfig(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_2MaxQueries_QueryConfig, + types.ExchangeQueryDetails{}, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + mutableExchangeConfig := pf.mutableState.GetMutableExchangeConfig() + + require.NotSame(t, mutableExchangeConfig, pf.mutableState.mutableExchangeConfig) + require.Equal(t, pf.mutableState.mutableExchangeConfig, mutableExchangeConfig) +} + +// TestGetTaskLoopDefinition asserts that the task loop definition is correctly +// set from mutable state and that it uses copies of all identical data structures. +func TestGetTaskLoopDefinition(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_2MaxQueries_QueryConfig, + types.ExchangeQueryDetails{}, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + taskLoopDefinition := pf.getTaskLoopDefinition() + + // The taskLoopDefinition should use copies of shared state + require.NotSame(t, pf.mutableState.mutableExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.NotSame(t, pf.mutableState.marketExponents, taskLoopDefinition.marketExponents) + + require.Equal(t, pf.mutableState.mutableExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, pf.mutableState.marketExponents, taskLoopDefinition.marketExponents) + require.Equal(t, []types.MarketId{7, 8}, taskLoopDefinition.marketIds) +} diff --git a/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go b/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go new file mode 100644 index 000000000..ab35a8521 --- /dev/null +++ b/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go @@ -0,0 +1,841 @@ +package price_fetcher + +import ( + "errors" + "testing" + + daemontypes "github.com/tellor-io/layer/daemons/types" + + "cosmossdk.io/math" + pricefeed_cosntants "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" + + "github.com/cometbft/cometbft/libs/log" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + "github.com/tellor-io/layer/lib" + "github.com/tellor-io/layer/mocks" + "github.com/tellor-io/layer/testutil/constants" +) + +const ( + taskLoopIterations = 2 +) + +var ( + exchangeQueryHandlerFailure = errors.New("Failed to query exchange") + tickerNotAvailable = errors.New("Ticker not listed") +) + +// TestRunTaskLoop tests that different exchange configurations results in the expected queries being made, and prices +// produced. +func TestRunTaskLoop(t *testing.T) { + tests := map[string]struct { + // parameters + queryConfig types.ExchangeQueryConfig + queryDetails types.ExchangeQueryDetails + mutableExchangeConfig types.MutableExchangeMarketConfig + mutableMarketConfigs []*types.MutableMarketConfig + + // expectations + expectedMarketIdsCalled []types.MarketId + }{ + "No markets": { + queryConfig: constants.Exchange1_0MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_NoMarkets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_0Markets, + }, + "Num markets equals max query markets where there is only 1 market": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_1Markets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_1Markets, + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId7, + }, + }, + "Num markets < max query markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_1Markets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_1Markets, + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId7, + }, + }, + "Num markets equals max query markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_2Markets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_2Markets, + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId7, + constants.MarketId8, + }, + }, + "Multi-market, 2 markets": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.MultiMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_2Markets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_2Markets, + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId7, + constants.MarketId8, + }, + }, + "Num markets greater than max query markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_3Markets, + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId7, + }, + }, + "Multi-market, 5 markets": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.MultiMarketExchangeQueryDetails, + mutableExchangeConfig: constants.Exchange1_5Markets_MutableExchangeMarketConfig, + mutableMarketConfigs: constants.MutableMarketConfigs_5Markets, + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId10, + constants.MarketId11, + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId10, + constants.MarketId11, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup for testing. + bCh := newTestPriceFetcherBufferedChannel() + + queryHandler := generateMockExchangeQueryHandler() + + pf, err := NewPriceFetcher( + tc.queryConfig, + tc.queryDetails, + &tc.mutableExchangeConfig, + tc.mutableMarketConfigs, + queryHandler, + log.NewNopLogger(), + bCh, + ) + require.NoError(t, err) + + // Run sub-task a specified number of iterations. + for i := 0; i < taskLoopIterations; i++ { + pf.RunTaskLoop(&daemontypes.RequestHandlerImpl{}) + } + + // Will hang until tests timeout if bCh is not full. + var bufferedResponses []*types.MarketPriceTimestamp + for len(bufferedResponses) < len(tc.expectedMarketIdsCalled) { + // Will block until test times out if bCh is not written to `tc.expectedMarketIdsCalled` + // times. + val := <-bCh + bufferedResponses = append(bufferedResponses, val.Price) + require.NoError(t, val.Err) + } + close(bCh) + + expectedBChValues := make([]*types.MarketPriceTimestamp, 0, len(tc.expectedMarketIdsCalled)) + for _, market := range tc.expectedMarketIdsCalled { + expectedBChValues = append(expectedBChValues, constants.CanonicalMarketPriceTimestampResponses[market]) + } + + // Verify contents of buffered channel. + require.ElementsMatch( + t, + expectedBChValues, + bufferedResponses, + ) + + // Verify each go routine was called as expected. + // NOTE: ordering of calls is not checked in `AssertCalled`. + expectedQueries := len(tc.expectedMarketIdsCalled) + marketsPerCall := 1 + if tc.queryDetails.IsMultiMarket { + expectedQueries = taskLoopIterations + marketsPerCall = len(tc.mutableExchangeConfig.MarketToMarketConfig) + } + queryHandler.AssertNumberOfCalls(t, "Query", expectedQueries) + for i := 0; i < len(tc.expectedMarketIdsCalled); i = i + marketsPerCall { + assertQueryHandlerCalledWithMarkets( + t, + queryHandler, + tc.expectedMarketIdsCalled[i:i+marketsPerCall], + tc.mutableMarketConfigs, + ) + } + }) + } +} + +// TestGetTaskLoopDefinition_SingleMarketExchange tests that the `getTaskLoopDefinition` method returns the +// expected task loop definition for a single market exchange, and that it properly advances through the +// market ids for an exchange on every call. +func TestGetTaskLoopDefinition_SingleMarketExchange(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_2MaxQueries_QueryConfig, + constants.SingleMarketExchangeQueryDetails, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + expectedExchangeConfig := &constants.Exchange1_3Markets_MutableExchangeMarketConfig + expectedMarketExponents := generateMarketExponentsMap(constants.MutableMarketConfigs_3Markets) + + taskLoopDefinition := pf.getTaskLoopDefinition() + + // Expect that the definition uses a copy of the mutableExchangeConfig for synchronization purposes. + require.NotSame(t, pf.mutableState.mutableExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, expectedExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, expectedMarketExponents, taskLoopDefinition.marketExponents) + require.Equal(t, []types.MarketId{constants.MarketId7, constants.MarketId8}, taskLoopDefinition.marketIds) + + // Expect that the market ids ring has been advanced by 2. + taskLoopDefinition = pf.getTaskLoopDefinition() + + // Sanity checks: + + require.NotSame(t, pf.mutableState.mutableExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, expectedExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, expectedMarketExponents, taskLoopDefinition.marketExponents) + + // Test that the markets have changed as expected. + require.Equal(t, []types.MarketId{constants.MarketId9, constants.MarketId7}, taskLoopDefinition.marketIds) +} + +// TestGetTaskLoopDefinition_MultiMarketExchange tests that the `getTaskLoopDefinition` method returns the +// expected task loop definition for a multi-market exchange. +func TestGetTaskLoopDefinition_MultiMarketExchange(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_1MaxQueries_QueryConfig, + constants.MultiMarketExchangeQueryDetails, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + + taskLoopDefinition := pf.getTaskLoopDefinition() + expectedMarkets := constants.Exchange1_3Markets_MutableExchangeMarketConfig.GetMarketIds() + expectedExponents := generateMarketExponentsMap(constants.MutableMarketConfigs_3Markets) + expectedExchangeConfig := &constants.Exchange1_3Markets_MutableExchangeMarketConfig + + // Expect that the definition uses a copy of the mutableExchangeConfig for synchronization purposes. + require.NotSame(t, expectedExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, expectedExchangeConfig, taskLoopDefinition.mutableExchangeConfig) + require.Equal(t, expectedExponents, taskLoopDefinition.marketExponents) + require.Equal(t, expectedMarkets, taskLoopDefinition.marketIds) +} + +// TestUpdateMutableExchangeConfig_CorrectlyUpdatesTaskDefinition tests that the `updateMutableExchangeConfig` method +// changes the price fetcher's state correctly so that the next call to `getTaskLoopDefinition` returns the expected +// definition. +func TestUpdateMutableExchangeConfig_CorrectlyUpdatesTaskDefinition(t *testing.T) { + tests := map[string]struct { + // parameters + queryConfig types.ExchangeQueryConfig + queryDetails types.ExchangeQueryDetails + + initialMutableExchangeConfig types.MutableExchangeMarketConfig + initialMarketConfig []*types.MutableMarketConfig + + updateMutableExchangeConfig types.MutableExchangeMarketConfig + updateMarketConfig []*types.MutableMarketConfig + + isMultiMarket bool + + // expectations + initialExpectedExponents map[types.MarketId]types.Exponent + updateExpectedExponents map[types.MarketId]types.Exponent + }{ + "Multimarket: No markets to markets": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.MultiMarketExchangeQueryDetails, + isMultiMarket: true, + + initialMutableExchangeConfig: constants.Exchange1_NoMarkets_MutableExchangeMarketConfig, + initialMarketConfig: constants.MutableMarketConfigs_0Markets, + initialExpectedExponents: map[types.MarketId]types.Exponent{}, + + updateMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + updateMarketConfig: constants.MutableMarketConfigs_3Markets, + updateExpectedExponents: constants.MutableMarketConfigs_3Markets_ExpectedExponents, + }, + "Multimarket: Add markets": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.MultiMarketExchangeQueryDetails, + isMultiMarket: true, + + initialMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + initialMarketConfig: constants.MutableMarketConfigs_3Markets, + initialExpectedExponents: constants.MutableMarketConfigs_3Markets_ExpectedExponents, + + updateMutableExchangeConfig: constants.Exchange1_5Markets_MutableExchangeMarketConfig, + updateMarketConfig: constants.MutableMarketConfigs_5Markets, + updateExpectedExponents: constants.MutableMarketConfigs_5Markets_ExpectedExponents, + }, + "Single market: No markets to markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + + initialMutableExchangeConfig: constants.Exchange1_NoMarkets_MutableExchangeMarketConfig, + initialMarketConfig: constants.MutableMarketConfigs_0Markets, + initialExpectedExponents: map[types.MarketId]types.Exponent{}, + + updateMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + updateMarketConfig: constants.MutableMarketConfigs_3Markets, + updateExpectedExponents: constants.MutableMarketConfigs_3Markets_ExpectedExponents, + }, + "Single market: Add markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + + initialMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + initialMarketConfig: constants.MutableMarketConfigs_3Markets, + initialExpectedExponents: constants.MutableMarketConfigs_3Markets_ExpectedExponents, + + updateMutableExchangeConfig: constants.Exchange1_5Markets_MutableExchangeMarketConfig, + updateMarketConfig: constants.MutableMarketConfigs_5Markets, + updateExpectedExponents: constants.MutableMarketConfigs_5Markets_ExpectedExponents, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup for testing. + bCh := newTestPriceFetcherBufferedChannel() + + queryHandler := generateMockExchangeQueryHandler() + + pf, err := NewPriceFetcher( + tc.queryConfig, + tc.queryDetails, + &tc.initialMutableExchangeConfig, + tc.initialMarketConfig, + queryHandler, + log.NewNopLogger(), + bCh, + ) + require.NoError(t, err) + + taskLoopDefinition := pf.getTaskLoopDefinition() + + require.Equal(t, tc.initialExpectedExponents, taskLoopDefinition.marketExponents) + require.Equal(t, tc.initialMutableExchangeConfig, *taskLoopDefinition.mutableExchangeConfig) + + if tc.isMultiMarket || len(tc.initialMarketConfig) == 0 { + require.Equal(t, tc.initialMutableExchangeConfig.GetMarketIds(), taskLoopDefinition.marketIds) + } else { + numMarkets := lib.Min(len(tc.initialMarketConfig), int(tc.queryConfig.MaxQueries)) + require.Len(t, taskLoopDefinition.marketIds, numMarkets) + + for i := 0; i < numMarkets; i++ { + require.Equal(t, tc.initialMarketConfig[i].Id, taskLoopDefinition.marketIds[i]) + } + } + + err = pf.UpdateMutableExchangeConfig(&tc.updateMutableExchangeConfig, tc.updateMarketConfig) + require.NoError(t, err) + + taskLoopDefinition = pf.getTaskLoopDefinition() + + require.Equal(t, tc.updateExpectedExponents, taskLoopDefinition.marketExponents) + require.Equal(t, tc.updateMutableExchangeConfig, *taskLoopDefinition.mutableExchangeConfig) + + if tc.isMultiMarket { + require.Equal(t, tc.updateMutableExchangeConfig.GetMarketIds(), taskLoopDefinition.marketIds) + } else { + numMarkets := lib.Min(len(tc.initialMarketConfig), int(tc.queryConfig.MaxQueries)) + require.Len(t, taskLoopDefinition.marketIds, int(tc.queryConfig.MaxQueries)) + + for i := 0; i < numMarkets; i++ { + require.Equal(t, tc.updateMarketConfig[i].Id, taskLoopDefinition.marketIds[i]) + } + } + }) + } +} + +// TestUpdateMutableExchangeConfig_ProducesExpectedPrices tests that updating the price fetcher's mutable +// config produces the expected prices before and after the update, with no errors. This test validates +// that the query handler receives all the expected inputs to resolve the expected market prices. +func TestUpdateMutableExchangeConfig_ProducesExpectedPrices(t *testing.T) { + tests := map[string]struct { + // parameters + queryConfig types.ExchangeQueryConfig + queryDetails types.ExchangeQueryDetails + + initialMutableExchangeConfig types.MutableExchangeMarketConfig + initialMarketConfigs []*types.MutableMarketConfig + + updateMutableExchangeConfig types.MutableExchangeMarketConfig + updateMarketConfigs []*types.MutableMarketConfig + + isMultiMarket bool + + // expectations + expectedMarketIdsCalled []types.MarketId + expectedNumQueryCalls int + }{ + "Multimarket: No markets to markets": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.MultiMarketExchangeQueryDetails, + isMultiMarket: true, + + initialMutableExchangeConfig: constants.Exchange1_NoMarkets_MutableExchangeMarketConfig, + initialMarketConfigs: constants.MutableMarketConfigs_0Markets, + + updateMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + updateMarketConfigs: constants.MutableMarketConfigs_3Markets, + + // Expect to loop through both sets of markets twice. The first set is empty. + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + }, + // We don't expect query calls for the initial empty set of markets. + expectedNumQueryCalls: 2, + }, + "Multimarket: Add markets": { + queryConfig: constants.Exchange1_1MaxQueries_QueryConfig, + queryDetails: constants.MultiMarketExchangeQueryDetails, + isMultiMarket: true, + + initialMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + initialMarketConfigs: constants.MutableMarketConfigs_3Markets, + + updateMutableExchangeConfig: constants.Exchange1_5Markets_MutableExchangeMarketConfig, + updateMarketConfigs: constants.MutableMarketConfigs_5Markets, + + // Expect to loop through both sets of markets twice. + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId10, + constants.MarketId11, + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId10, + constants.MarketId11, + }, + // Multi-market exchanges are expected to call 1x per task loop run. + expectedNumQueryCalls: 4, + }, + "Single market: No markets to markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + + initialMutableExchangeConfig: constants.Exchange1_NoMarkets_MutableExchangeMarketConfig, + initialMarketConfigs: constants.MutableMarketConfigs_0Markets, + + updateMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + updateMarketConfigs: constants.MutableMarketConfigs_3Markets, + + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId7, + }, + // Single-market exchanges are expected to call up to max queries per task loop run. + expectedNumQueryCalls: 4, + }, + "Single market: Add markets": { + queryConfig: constants.Exchange1_2MaxQueries_QueryConfig, + queryDetails: constants.SingleMarketExchangeQueryDetails, + + initialMutableExchangeConfig: constants.Exchange1_3Markets_MutableExchangeMarketConfig, + initialMarketConfigs: constants.MutableMarketConfigs_3Markets, + + updateMutableExchangeConfig: constants.Exchange1_5Markets_MutableExchangeMarketConfig, + updateMarketConfigs: constants.MutableMarketConfigs_5Markets, + + expectedMarketIdsCalled: []types.MarketId{ + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId7, + constants.MarketId7, + constants.MarketId8, + constants.MarketId9, + constants.MarketId10, + }, + // Single-market exchanges are expected to call up to max queries per task loop run. + expectedNumQueryCalls: 8, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Setup for testing. + bCh := newTestPriceFetcherBufferedChannel() + queryHandler := generateMockExchangeQueryHandler() + pf, err := NewPriceFetcher( + tc.queryConfig, + tc.queryDetails, + &tc.initialMutableExchangeConfig, + tc.initialMarketConfigs, + queryHandler, + log.NewNopLogger(), + bCh, + ) + require.NoError(t, err) + + // Run sub-task a specified number of iterations. + for i := 0; i < taskLoopIterations; i++ { + pf.RunTaskLoop(&daemontypes.RequestHandlerImpl{}) + } + + // No race conditions should affect the market output of the previous or following task loops. + err = pf.UpdateMutableExchangeConfig(&tc.updateMutableExchangeConfig, tc.updateMarketConfigs) + require.NoError(t, err) + + // Run sub-task a specified number of iterations. + for i := 0; i < taskLoopIterations; i++ { + go pf.RunTaskLoop(&daemontypes.RequestHandlerImpl{}) + } + + // Will hang until tests timeout if bCh is not full. + var bufferedResponses []*types.MarketPriceTimestamp + for len(bufferedResponses) < len(tc.expectedMarketIdsCalled) { + // Will block until test times out if bCh is not written to `tc.expectedMarketIdsCalled` + // times. + val := <-bCh + bufferedResponses = append(bufferedResponses, val.Price) + require.NoError(t, val.Err) + } + close(bCh) + + expectedBChValues := make([]*types.MarketPriceTimestamp, 0, len(tc.expectedMarketIdsCalled)) + for _, market := range tc.expectedMarketIdsCalled { + expectedBChValues = append(expectedBChValues, constants.CanonicalMarketPriceTimestampResponses[market]) + } + + // Verify contents of buffered channel. + require.ElementsMatch( + t, + expectedBChValues, + bufferedResponses, + ) + + queryHandler.AssertNumberOfCalls(t, "Query", tc.expectedNumQueryCalls) + if tc.isMultiMarket { + // For multi market exchanges, the query handler should be called once per task loop, and each + // query should be for all markets supported by the exchange at that time. + initialNumMarkets := len(tc.initialMutableExchangeConfig.MarketToMarketConfig) + updateNumMarkets := len(tc.updateMutableExchangeConfig.MarketToMarketConfig) + + indexPtr := 0 + if initialNumMarkets > 0 { + for i := 0; i < taskLoopIterations; i++ { + assertQueryHandlerCalledWithMarkets( + t, + queryHandler, + tc.expectedMarketIdsCalled[indexPtr:indexPtr+initialNumMarkets], + tc.initialMarketConfigs, + ) + indexPtr += initialNumMarkets + } + } + for i := 0; i < taskLoopIterations; i++ { + assertQueryHandlerCalledWithMarkets( + t, + queryHandler, + tc.expectedMarketIdsCalled[indexPtr:indexPtr+updateNumMarkets], + tc.updateMarketConfigs, + ) + indexPtr += updateNumMarkets + } + } else { + // For single market exchanges, the query handler should be called once per market per task loop. + marketsPerInitialConfig := math.Min( + len(tc.initialMarketConfigs), + int(tc.queryConfig.MaxQueries), + ) * 2 + for i, marketId := range tc.expectedMarketIdsCalled { + marketConfigs := tc.initialMarketConfigs + if i >= marketsPerInitialConfig { + marketConfigs = tc.updateMarketConfigs + } + assertQueryHandlerCalledWithMarkets( + t, + queryHandler, + []types.MarketId{marketId}, + marketConfigs, + ) + } + } + }) + } +} + +func TestGetExchangeId(t *testing.T) { + pf, err := NewPriceFetcher( + constants.Exchange1_1MaxQueries_QueryConfig, + constants.MultiMarketExchangeQueryDetails, + &constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + &mocks.ExchangeQueryHandler{}, + log.NewNopLogger(), + newTestPriceFetcherBufferedChannel(), + ) + require.NoError(t, err) + require.Equal(t, constants.Exchange1_1MaxQueries_QueryConfig.ExchangeId, pf.GetExchangeId()) +} + +// Test runSubTask behavior with different query handler responses +func TestRunSubTask_Mixed(t *testing.T) { + tests := map[string]struct { + responsePriceTimestamps []*types.MarketPriceTimestamp + responseUnavailableMarkets map[types.MarketId]error + responseError error + + expectedPrices []*types.MarketPriceTimestamp + expectedErrors []error + }{ + "Failure - failed to query exchange": { + responseError: exchangeQueryHandlerFailure, + expectedErrors: []error{exchangeQueryHandlerFailure}, + }, + "Mixed - returned prices have a 0": { + responsePriceTimestamps: []*types.MarketPriceTimestamp{ + { + MarketId: 7, + Price: 0, + }, + constants.Market8_TimeT_Price1, + }, + expectedPrices: []*types.MarketPriceTimestamp{ + constants.Market8_TimeT_Price1, + }, + expectedErrors: []error{ + errors.New("Invalid price of 0 for exchange: 'Exchange1' and market: 7"), + }, + }, + "Mixed - unavailable tickers": { + responsePriceTimestamps: []*types.MarketPriceTimestamp{ + constants.Market8_TimeT_Price1, + }, + responseUnavailableMarkets: map[types.MarketId]error{ + constants.MarketId8: tickerNotAvailable, + }, + expectedPrices: []*types.MarketPriceTimestamp{ + constants.Market8_TimeT_Price1, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + exchangeQueryConfig := constants.Exchange1_1MaxQueries_QueryConfig + mutableExchangeMarketConfig := constants.Exchange1_1Markets_MutableExchangeMarketConfig + mutableMarketConfigs := constants.MutableMarketConfigs_1Markets + mockExchangeQueryHandler := &mocks.ExchangeQueryHandler{} + rh := &daemontypes.RequestHandlerImpl{} + + mockExchangeQueryHandler.On( + "Query", + mock.AnythingOfType("*context.timerCtx"), + mock.AnythingOfType("*types.ExchangeQueryDetails"), + &mutableExchangeMarketConfig, + mutableExchangeMarketConfig.GetMarketIds(), + rh, + generateMarketExponentsMap(mutableMarketConfigs), + ). + Return(tc.responsePriceTimestamps, tc.responseUnavailableMarkets, tc.responseError) + + // Setup for sub-task iterations. + bCh := newTestPriceFetcherBufferedChannel() + + pf, err := NewPriceFetcher( + exchangeQueryConfig, + constants.MultiMarketExchangeQueryDetails, + &mutableExchangeMarketConfig, + mutableMarketConfigs, + mockExchangeQueryHandler, + log.NewNopLogger(), + bCh, + ) + require.NoError(t, err) + + // We just need a valid input that matches the mock signature. + pf.runSubTask( + &daemontypes.RequestHandlerImpl{}, + mutableExchangeMarketConfig.GetMarketIds(), + pf.getTaskLoopDefinition(), + ) + + actualErrors := make([]error, 0, len(tc.expectedErrors)) + var actualPrices []*types.MarketPriceTimestamp + if len(tc.expectedPrices) > 0 { + actualPrices = make([]*types.MarketPriceTimestamp, 0, len(tc.expectedPrices)) + } + + for i := 0; i < len(tc.expectedErrors)+len(tc.expectedPrices); i++ { + value := <-bCh + if value.Err != nil { + actualErrors = append(actualErrors, value.Err) + } else { + actualPrices = append(actualPrices, value.Price) + } + } + require.Equal(t, tc.expectedPrices, actualPrices) + pricefeed.ErrorsEqual(t, tc.expectedErrors, actualErrors) + }) + } +} + +// ----------------- Generate Mock Instances ----------------- // +func generateMockExchangeQueryHandler() *mocks.ExchangeQueryHandler { + mockExchangeQueryHandler := &mocks.ExchangeQueryHandler{} + mockSingleMarketCalls(mockExchangeQueryHandler) + // Mock multi-market call for 2 market test exchanges. + mockMultiMarketCall( + constants.Exchange1_2Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_2Markets, + mockExchangeQueryHandler, + ) + // Mock multi-market call for 3 market test exchanges. + mockMultiMarketCall( + constants.Exchange1_3Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_3Markets, + mockExchangeQueryHandler, + ) + // Mock multi-market call for 5 market test exchanges. + mockMultiMarketCall( + constants.Exchange1_5Markets_MutableExchangeMarketConfig, + constants.MutableMarketConfigs_5Markets, + mockExchangeQueryHandler, + ) + + return mockExchangeQueryHandler +} + +// generateMarketExponentsMap generates a map of marketId to exponent for the given marketConfigs. This is +// the exponents map we would expect to be passed to the exchange query handler. +func generateMarketExponentsMap(marketConfigs []*types.MutableMarketConfig) map[types.MarketId]types.Exponent { + marketExponents := make(map[types.MarketId]types.Exponent, len(marketConfigs)) + for _, marketConfigs := range marketConfigs { + marketExponents[marketConfigs.Id] = marketConfigs.Exponent + } + return marketExponents +} + +func mockSingleMarketCalls(mockExchangeQueryHandler *mocks.ExchangeQueryHandler) { + // Support single market calls for all possible marketConfigs paired with the test exchange configs. + initialMarketConfigsList := [][]*types.MutableMarketConfig{ + constants.MutableMarketConfigs_1Markets, + constants.MutableMarketConfigs_2Markets, + constants.MutableMarketConfigs_3Markets, + constants.MutableMarketConfigs_5Markets, + } + for marketId, priceTimestamp := range constants.CanonicalMarketPriceTimestampResponses { + for _, initialMarketConfigs := range initialMarketConfigsList { + mockExchangeQueryHandler.On( + "Query", + mock.AnythingOfType("*context.timerCtx"), + mock.AnythingOfType("*types.ExchangeQueryDetails"), + mock.AnythingOfType("*types.MutableExchangeMarketConfig"), + []types.MarketId{marketId}, + &daemontypes.RequestHandlerImpl{}, + generateMarketExponentsMap(initialMarketConfigs), + ).Return([]*types.MarketPriceTimestamp{priceTimestamp}, nil, nil) + } + } +} + +func mockMultiMarketCall( + mutableExchangeMarketConfig types.MutableExchangeMarketConfig, + mutableMarketConfigs []*types.MutableMarketConfig, + mockExchangeQueryHandler *mocks.ExchangeQueryHandler, +) { + markets := mutableExchangeMarketConfig.GetMarketIds() + prices := make([]*types.MarketPriceTimestamp, 0, len(markets)) + for _, market := range markets { + prices = append(prices, constants.CanonicalMarketPriceTimestampResponses[market]) + } + + mockExchangeQueryHandler.On( + "Query", + mock.AnythingOfType("*context.timerCtx"), + mock.AnythingOfType("*types.ExchangeQueryDetails"), + mock.AnythingOfType("*types.MutableExchangeMarketConfig"), + markets, + &daemontypes.RequestHandlerImpl{}, + generateMarketExponentsMap(mutableMarketConfigs), + ).Return(prices, nil, nil) +} + +// ----------------- Helper Functions ----------------- // + +// newTestPriceFetcherBufferedChannel returns a buffered channel with the default fixed size, suitably +// large enough to hold all the responses from multiple sub-task runs. +func newTestPriceFetcherBufferedChannel() chan *PriceFetcherSubtaskResponse { + bCh := make( + chan *PriceFetcherSubtaskResponse, + pricefeed_cosntants.FixedBufferSize, + ) + + return bCh +} + +// asserQueryHandlerCalledWithMarkets asserts that the query handler was called with the expected markets. +func assertQueryHandlerCalledWithMarkets( + t *testing.T, + queryHandler *mocks.ExchangeQueryHandler, + markets []types.MarketId, + marketConfigs []*types.MutableMarketConfig, +) { + marketExponents := make(map[types.MarketId]types.Exponent) + for _, market := range markets { + marketExponents[market] = constants.CanonicalMarketExponents[market] + } + queryHandler.AssertCalled( + t, + "Query", + mock.Anything, + mock.AnythingOfType("*types.ExchangeQueryDetails"), + mock.AnythingOfType("*types.MutableExchangeMarketConfig"), + markets, + &daemontypes.RequestHandlerImpl{}, + generateMarketExponentsMap(marketConfigs), + ) +} diff --git a/daemons/pricefeed/client/queryhandler/exchange_query_handler_integration_test.go b/daemons/pricefeed/client/queryhandler/exchange_query_handler_integration_test.go new file mode 100644 index 000000000..95d0ba926 --- /dev/null +++ b/daemons/pricefeed/client/queryhandler/exchange_query_handler_integration_test.go @@ -0,0 +1,52 @@ +//go:build all || exchange_tests + +package handler + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/kraken" + "github.com/tellor-io/layer/daemons/types" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/binance" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources/bitfinex" +) + +func TestQueryingActualExchanges(t *testing.T) { + tests := map[string]struct { + // parameters + url string + }{ + "Binance": { + url: CreateRequestUrl(binance.BinanceDetails.Url, []string{`"ETHUSDT"`}), + }, + "BinanceUS": { + url: CreateRequestUrl(binance.BinanceUSDetails.Url, []string{`"BTCUSD"`}), + }, + "Bitfinex": { + url: CreateRequestUrl(bitfinex.BitfinexDetails.Url, []string{"tBTCUSD"}), + }, + "Kraken": { + url: CreateRequestUrl(kraken.KrakenDetails.Url, []string{"XXBTZUSD", "XETHZUSD", "LINKUSD"}), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + requestHandler := types.NewRequestHandlerImpl(http.DefaultClient) + + response, err := requestHandler.Get(context.Background(), tc.url) + + if response.StatusCode != 200 { + fmt.Println(response) + } + + require.NoError(t, err) + require.Equal(t, 200, response.StatusCode) + }) + } +} diff --git a/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go b/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go new file mode 100644 index 000000000..be9e7e280 --- /dev/null +++ b/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go @@ -0,0 +1,372 @@ +package handler + +import ( + "context" + "errors" + "fmt" + "net/http" + "testing" + "time" + + "github.com/tellor-io/layer/testutil/daemons/pricefeed/exchange_config" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + pf_constants "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/pricefeed/client/sources" + "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pft "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/mocks" + "github.com/tellor-io/layer/testutil/constants" + "github.com/tellor-io/layer/testutil/daemons/pricefeed" +) + +const ( + successStatus = 200 + failStatus400 = 400 + failStatus500 = 500 + dummyPrice = uint64(1) + noPriceExponentMarketId = 100000 + FAKEUSD_ID = 100001 + unavailableId = 100002 + // No price exponent exists for this fake pair. + noPriceExponentTicker = "INVALID-USD" + noMarketTicker = "NO-MARKET-SYMBOL" + unavailableTicker = "UNAVAILABLE" + unavailableExponent = -6 +) + +var ( + queryError = errors.New("Failed to query exchange") + priceFuncError = errors.New("Failed to get Price") + tickerNotAvailableError = errors.New("Ticker not available") + baseEqd = &types.ExchangeQueryDetails{ + Url: "https://api.binance.us/api/v3/ticker/24hr?symbol=$", + } + baseEmc = &types.MutableExchangeMarketConfig{ + Id: "BinanceUS", + MarketToMarketConfig: map[types.MarketId]types.MarketConfig{ + exchange_config.MARKET_BTC_USD: { + Ticker: constants.BtcUsdPair, + }, + exchange_config.MARKET_ETH_USD: { + Ticker: constants.EthUsdPair, + }, + noPriceExponentMarketId: { + Ticker: noPriceExponentTicker, + }, + unavailableId: { + Ticker: unavailableTicker, + }, + }, + } + testMarketExponentMap = generateTestMarketPriceExponentMap() +) + +func TestQuery(t *testing.T) { + lastUpdatedAt := time.Unix(0, 0) + eqh := ExchangeQueryHandlerImpl{generateMockTimeProvider(lastUpdatedAt)} + + tests := map[string]struct { + // parameters + priceFunc func( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pft.Resolver, + ) (prices map[string]uint64, unavailable map[string]error, err error) + marketIds []types.MarketId + requestHandler *mocks.RequestHandler + + // expectations + expectedPrices []*types.MarketPriceTimestamp + expectedUnavailable map[types.MarketId]error + expectApiRequest bool + expectedError error + }{ + "Success - single market": { + priceFunc: priceFunc, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + successStatus, + nil, + ), + expectApiRequest: true, + expectedPrices: []*types.MarketPriceTimestamp{ + { + Price: dummyPrice, + MarketId: exchange_config.MARKET_BTC_USD, + LastUpdatedAt: lastUpdatedAt, + }, + }, + }, + "Success - multiple markets": { + priceFunc: priceFunc, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD, exchange_config.MARKET_ETH_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{ + constants.BtcUsdPair, + constants.EthUsdPair, + }), + successStatus, + nil, + ), + expectApiRequest: true, + expectedPrices: []*types.MarketPriceTimestamp{ + { + Price: dummyPrice, + MarketId: exchange_config.MARKET_BTC_USD, + LastUpdatedAt: lastUpdatedAt, + }, + { + Price: dummyPrice, + MarketId: exchange_config.MARKET_ETH_USD, + LastUpdatedAt: lastUpdatedAt, + }, + }, + }, + "Success - multiple markets and unavailable ticker": { + priceFunc: priceFuncWithValidAndUnavailableTickers, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD, exchange_config.MARKET_ETH_USD, unavailableId}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{ + constants.BtcUsdPair, + constants.EthUsdPair, + unavailableTicker, + }), + successStatus, + nil, + ), + expectApiRequest: true, + expectedPrices: []*types.MarketPriceTimestamp{ + { + Price: dummyPrice, + MarketId: exchange_config.MARKET_BTC_USD, + LastUpdatedAt: lastUpdatedAt, + }, + { + Price: dummyPrice, + MarketId: exchange_config.MARKET_ETH_USD, + LastUpdatedAt: lastUpdatedAt, + }, + }, + expectedUnavailable: map[types.MarketId]error{ + unavailableId: tickerNotAvailableError, + }, + }, + "Failure - price function returns non-existent unavailable ticker": { + priceFunc: priceFuncReturnsInvalidUnavailableTicker, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{ + constants.BtcUsdPair, + }), + successStatus, + nil, + ), + expectApiRequest: true, + expectedError: fmt.Errorf("Severe unexpected error: no market id for ticker: %s", noMarketTicker), + }, + "Failure - no marketIds queried": { + marketIds: []types.MarketId{}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + successStatus, + nil, + ), + expectApiRequest: false, + expectedError: errors.New("At least one marketId must be queried"), + }, + "Failure - market config not defined for market": { + marketIds: []types.MarketId{FAKEUSD_ID}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{}), + successStatus, + nil, + ), + expectApiRequest: false, + expectedError: fmt.Errorf("No market config for market: %v", FAKEUSD_ID), + }, + "Failure - market price exponent not defined for market": { + marketIds: []types.MarketId{noPriceExponentMarketId}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + successStatus, + nil, + ), + expectApiRequest: false, + expectedError: fmt.Errorf("No market price exponent for id: %v", noPriceExponentMarketId), + }, + "Failure - query fails": { + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + successStatus, + queryError, + ), + expectApiRequest: true, + expectedError: queryError, + }, + "Failure - unexpected API response code: 400": { + priceFunc: priceFunc, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + failStatus400, + nil, + ), + expectApiRequest: true, + expectedError: fmt.Errorf("%s %v", pf_constants.UnexpectedResponseStatusMessage, 400), + }, + "Failure - unexpected API response code: 500": { + priceFunc: priceFunc, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + failStatus500, + nil, + ), + expectApiRequest: true, + expectedError: fmt.Errorf("%s %v", pf_constants.UnexpectedResponseStatusMessage, 500), + }, + "Failure - PriceFunction fails": { + priceFunc: priceFuncWithErr, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{constants.BtcUsdPair}), + successStatus, + nil, + ), + expectApiRequest: true, + expectedError: sources.NewExchangeError("", priceFuncError.Error()), + }, + "Failure - PriceFunction returns invalid response": { + priceFunc: priceFuncWithInvalidResponse, + marketIds: []types.MarketId{exchange_config.MARKET_BTC_USD, exchange_config.MARKET_ETH_USD}, + requestHandler: generateMockRequestHandler( + CreateRequestUrl(baseEqd.Url, []string{ + constants.BtcUsdPair, + constants.EthUsdPair, + }), + successStatus, + nil, + ), + expectApiRequest: true, + expectedError: fmt.Errorf( + "Severe unexpected error: no market id for ticker: %v", + noMarketTicker, + ), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + baseEqd.PriceFunction = tc.priceFunc + + prices, unavailableMarkets, err := eqh.Query( + context.Background(), + baseEqd, + baseEmc, + tc.marketIds, + tc.requestHandler, + testMarketExponentMap, + ) + + if tc.expectApiRequest { + // Request argument is already tested in `generateMockRequestHandler`. + tc.requestHandler.AssertCalled(t, "Get", context.Background(), mock.Anything) + } else { + tc.requestHandler.AssertNotCalled(t, "Get") + } + + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + require.Nil(t, prices) + require.Nil(t, unavailableMarkets) + } else { + require.NoError(t, err) + require.ElementsMatch(t, tc.expectedPrices, prices) + pricefeed.ErrorMapsEqual(t, tc.expectedUnavailable, unavailableMarkets) + } + }) + } +} + +func generateMockTimeProvider(time time.Time) *mocks.TimeProvider { + mockTimeProvider := &mocks.TimeProvider{} + mockTimeProvider.On("Now").Return(time) + + return mockTimeProvider +} + +func generateMockRequestHandler(url string, statusCode int, err error) *mocks.RequestHandler { + mockRequestHandler := &mocks.RequestHandler{} + mockRequestHandler.On("Get", context.Background(), url).Return(&http.Response{StatusCode: statusCode}, err) + + return mockRequestHandler +} + +func generateTestMarketPriceExponentMap() map[types.MarketId]types.Exponent { + marketExponents := make(map[types.MarketId]types.Exponent, 6) + marketExponents[exchange_config.MARKET_BTC_USD] = constants.BtcUsdExponent + marketExponents[exchange_config.MARKET_ETH_USD] = constants.EthUsdExponent + marketExponents[exchange_config.MARKET_LINK_USD] = constants.LinkUsdExponent + marketExponents[exchange_config.MARKET_MATIC_USD] = constants.MaticUsdExponent + marketExponents[exchange_config.MARKET_CRV_USD] = constants.CrvUsdExponent + marketExponents[unavailableId] = unavailableExponent + return marketExponents +} + +func priceFunc( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pft.Resolver, +) (prices map[string]uint64, unavailable map[string]error, err error) { + prices = make(map[string]uint64, len(tickerToPriceExponent)) + for ticker := range tickerToPriceExponent { + prices[ticker] = dummyPrice + } + return prices, nil, nil +} + +func priceFuncWithInvalidResponse( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pft.Resolver, +) (prices map[string]uint64, unavailable map[string]error, err error) { + prices = make(map[string]uint64, len(tickerToPriceExponent)) + for range tickerToPriceExponent { + prices[noMarketTicker] = dummyPrice + } + return prices, nil, nil +} + +func priceFuncWithValidAndUnavailableTickers( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pft.Resolver, +) (prices map[string]uint64, unavailable map[string]error, err error) { + prices = make(map[string]uint64, len(tickerToPriceExponent)) + for ticker := range tickerToPriceExponent { + if ticker != unavailableTicker { + prices[ticker] = dummyPrice + } + } + return prices, map[string]error{unavailableTicker: tickerNotAvailableError}, nil +} + +func priceFuncReturnsInvalidUnavailableTicker( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pft.Resolver, +) (prices map[string]uint64, unavailable map[string]error, err error) { + return nil, map[string]error{noMarketTicker: tickerNotAvailableError}, nil +} + +func priceFuncWithErr( + response *http.Response, + tickerToPriceExponent map[string]int32, + resolver pft.Resolver, +) (prices map[string]uint64, unavailable map[string]error, err error) { + return nil, nil, priceFuncError +} diff --git a/daemons/pricefeed/types/price_timestamp_test.go b/daemons/pricefeed/types/price_timestamp_test.go new file mode 100644 index 000000000..8df0d40cb --- /dev/null +++ b/daemons/pricefeed/types/price_timestamp_test.go @@ -0,0 +1,100 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/pricefeed/types" + "github.com/tellor-io/layer/testutil/constants" +) + +func TestNewPriceTimestamp_IsEmpty(t *testing.T) { + pt := types.NewPriceTimestamp() + + require.Equal(t, time.Time{}, pt.LastUpdateTime) + require.Equal(t, uint64(0), pt.Price) +} + +func TestUpdatePrice_NoPreviousPriceSuccess(t *testing.T) { + pt := types.NewPriceTimestamp() + + // No previous Price exists + ok := pt.UpdatePrice(constants.Price1, &constants.TimeT) + require.True(t, ok) + + require.Equal(t, constants.TimeT, pt.LastUpdateTime) + require.Equal(t, constants.Price1, pt.Price) +} + +func TestUpdatePrice_GreaterUpdateTimeSuccess(t *testing.T) { + pt := types.NewPriceTimestamp() + + // Last update @ timeT + ok := pt.UpdatePrice(constants.Price1, &constants.TimeT) + require.True(t, ok) + + // New update @ timeT + threshold + ok = pt.UpdatePrice(constants.Price2, &constants.TimeTPlusThreshold) + require.True(t, ok) + + require.Equal(t, constants.TimeTPlusThreshold, pt.LastUpdateTime) + require.Equal(t, constants.Price2, pt.Price) +} + +func TestUpdatePrice_EqualUpdateTimeFail(t *testing.T) { + pt := types.NewPriceTimestamp() + + // Last update @ timeT + ok := pt.UpdatePrice(constants.Price1, &constants.TimeT) + require.True(t, ok) + + // New update @ timeT + ok = pt.UpdatePrice(constants.Price2, &constants.TimeT) + require.False(t, ok) + + // No update should be made because the new update time is not greater. + require.Equal(t, constants.TimeT, pt.LastUpdateTime) + require.Equal(t, constants.Price1, pt.Price) +} + +func TestUpdatePrice_SmallerUpdateTimeFail(t *testing.T) { + pt := types.NewPriceTimestamp() + + // Last update @ timeT + ok := pt.UpdatePrice(constants.Price1, &constants.TimeT) + require.True(t, ok) + + // New update @ timeT - threshold + ok = pt.UpdatePrice(constants.Price2, &constants.TimeTMinusThreshold) + require.False(t, ok) + + // No update should be made because the new update time is not greater. + require.Equal(t, constants.TimeT, pt.LastUpdateTime) + require.Equal(t, constants.Price1, pt.Price) +} + +func TestGetValidPrice_ValidLastUpdateTimeSuccess(t *testing.T) { + pt := types.NewPriceTimestamp() + + // Last update @ timeT + ok := pt.UpdatePrice(constants.Price1, &constants.TimeT) + require.True(t, ok) + + r, ok := pt.GetValidPrice(constants.TimeT) + require.True(t, ok) + require.Equal(t, constants.Price1, r) +} + +func TestGetValidPrice_InvalidLastUpdateTimeFail(t *testing.T) { + pt := types.NewPriceTimestamp() + + // Last update @ timeT + ok := pt.UpdatePrice(constants.Price1, &constants.TimeT) + require.True(t, ok) + + // Updates @ timeT are no longer valid at this cutoff time. + r, ok := pt.GetValidPrice(constants.TimeTPlus1) + require.False(t, ok) + require.Equal(t, uint64(0), r) +} diff --git a/daemons/readme.md b/daemons/readme.md new file mode 100644 index 000000000..4d992e49c --- /dev/null +++ b/daemons/readme.md @@ -0,0 +1,84 @@ +# Daemon gRPC Server + +## Overview + +Implements a gRPC server for daemon processes using Unix Domain Sockets (UDS) for communication. + +## Usage + +### Starting the Server + +```go +server := NewServer(logger, grpcServer, fileHandler, socketAddress) +server.Start() +``` + +### Stopping the server + +```go +server.Stop() +``` + +### Task loops + +## PriceFetcher + +- Will query exchanges for prices once or multiple times based on wether the api supports single vs multi markets; ie wether an api needs to be queried for each pair individually or can return multiple pairs at once, [See here for exchange details](./constants/static_exchange_details.go). + +## PriceEncoder + +- Will update cache with the queried prices and encode appropriately also make adjustments as necessary based on if adjustByMarket is defined. + +### Configuration + +## Exchange Config default + +```go +[[exchanges]] +ExchangeId = "Binance" // exchange identifier +IntervalMs = 2500 // Delays between sending api requests +TimeoutMs = 3000 // Max timeout +MaxQueries = 1 // Max number of calls in a loop. +``` + +Defaults for exchange information can be found [here](./configs/default_pricefeed_exchange_config.go) + +## Market Pair defauls + +```go +[[market_params]] +ExchangeConfigJson = "{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHBTC\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHBTC\",\"adjustByMarket\":\"BTC-USD\"}]}" +Exponent = -6 +Id = 2 +MinExchanges = 1 +MinPriceChangePpm = 1000 +Pair = "ETH-BTC" +``` + +Defaults for market pair can be found [here](./configs/default_market_param_config.go) + +```go +type MarketParam struct { + // Unique, sequentially-generated value. + Id uint32 + // The human-readable name of the market pair (e.g. `BTC-USD`). + Pair string + // Static value. The exponent of the price. + // For example if `Exponent == -5` then a `Value` of `1,000,000,000` + // represents “$10,000`. Therefore `10 ^ Exponent` represents the smallest + // price step (in dollars) that can be recorded. + Exponent int32 + // The minimum number of exchanges that should be reporting a live price for + // a price update to be considered valid. + MinExchanges uint32 + // The minimum allowable change in `price` value that would cause a price + // update on the network. Measured as `1e-6` (parts per million). + MinPriceChangePpm uint32 + // A string of json that encodes the configuration for resolving the price + // of this market on various exchanges. + ExchangeConfigJson string +} +``` + +Note: Price Daemon is enabled by default to disable set `--price-daemon-enabled=false` +TODO: Add mapping of queryData to Id diff --git a/daemons/server/pricefeed.go b/daemons/server/pricefeed.go index 7a355bad4..314b5e5ab 100644 --- a/daemons/server/pricefeed.go +++ b/daemons/server/pricefeed.go @@ -65,7 +65,7 @@ func (s *Server) UpdateMarketPrices( // Capture valid responses in metrics. s.reportValidResponse(types.PricefeedDaemonServiceName) - return &types.UpdateMarketPricesResponse{MarketPriceUpdates: req.MarketPriceUpdates}, nil + return &types.UpdateMarketPricesResponse{}, nil } // validateMarketPricesUpdatesMessage validates a `UpdateMarketPricesRequest`. diff --git a/daemons/server/types/pricefeed.pb.go b/daemons/server/types/pricefeed.pb.go index 8ae0de750..7f7bff570 100644 --- a/daemons/server/types/pricefeed.pb.go +++ b/daemons/server/types/pricefeed.pb.go @@ -80,7 +80,6 @@ func (m *UpdateMarketPricesRequest) GetMarketPriceUpdates() []*MarketPriceUpdate // UpdateMarketPricesResponse is a response message for updating market prices. type UpdateMarketPricesResponse struct { - MarketPriceUpdates []*MarketPriceUpdate `protobuf:"bytes,1,rep,name=market_price_updates,json=marketPriceUpdates,proto3" json:"market_price_updates,omitempty"` } func (m *UpdateMarketPricesResponse) Reset() { *m = UpdateMarketPricesResponse{} } @@ -116,13 +115,6 @@ func (m *UpdateMarketPricesResponse) XXX_DiscardUnknown() { var xxx_messageInfo_UpdateMarketPricesResponse proto.InternalMessageInfo -func (m *UpdateMarketPricesResponse) GetMarketPriceUpdates() []*MarketPriceUpdate { - if m != nil { - return m.MarketPriceUpdates - } - return nil -} - // ExchangePrice represents a specific exchange's market price type ExchangePrice struct { ExchangeId string `protobuf:"bytes,1,opt,name=exchange_id,json=exchangeId,proto3" json:"exchange_id,omitempty"` @@ -247,35 +239,34 @@ func init() { func init() { proto.RegisterFile("layer/daemons/pricefeed.proto", fileDescriptor_f18ab54548ab09c4) } var fileDescriptor_f18ab54548ab09c4 = []byte{ - // 436 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x53, 0x41, 0x6f, 0xd3, 0x30, - 0x14, 0xae, 0x37, 0x40, 0x9b, 0xab, 0x8e, 0x61, 0xf5, 0x10, 0xc2, 0x48, 0xa3, 0x9c, 0xc2, 0x01, - 0x5b, 0x2a, 0xff, 0x60, 0xd2, 0x90, 0x7a, 0x00, 0x21, 0x03, 0x17, 0x2e, 0x91, 0xdb, 0xbc, 0x65, - 0xd1, 0x92, 0xd8, 0xd8, 0xce, 0x60, 0x27, 0xfe, 0xc2, 0x4e, 0xfc, 0xa6, 0x1d, 0x77, 0xe4, 0x04, - 0xa8, 0xfd, 0x23, 0x28, 0x76, 0x8a, 0xd6, 0x0e, 0x24, 0x2e, 0xdc, 0xfc, 0xbe, 0xef, 0xcb, 0x7b, - 0xdf, 0xfb, 0x1c, 0xe3, 0xa7, 0x95, 0xb8, 0x04, 0xcd, 0x72, 0x01, 0xb5, 0x6c, 0x0c, 0x53, 0xba, - 0x5c, 0xc0, 0x29, 0x40, 0x4e, 0x95, 0x96, 0x56, 0x92, 0x91, 0xa3, 0x69, 0x4f, 0x87, 0xe3, 0x42, - 0x16, 0xd2, 0x31, 0xac, 0x3b, 0x79, 0x51, 0x78, 0x54, 0x48, 0x59, 0x54, 0xc0, 0x84, 0x2a, 0x99, - 0x68, 0x1a, 0x69, 0x85, 0x2d, 0x65, 0x63, 0x7a, 0x76, 0xd2, 0xb3, 0xae, 0x9a, 0xb7, 0xa7, 0xcc, - 0x96, 0x35, 0x18, 0x2b, 0x6a, 0xe5, 0x05, 0x89, 0xc4, 0x8f, 0xdf, 0xab, 0x5c, 0x58, 0x78, 0x25, - 0xf4, 0x39, 0xd8, 0x37, 0x9d, 0x05, 0xc3, 0xe1, 0x63, 0x0b, 0xc6, 0x12, 0x8e, 0xc7, 0xb5, 0x83, - 0x33, 0x67, 0x2d, 0x6b, 0x9d, 0xd2, 0x04, 0x28, 0xde, 0x4d, 0x87, 0xd3, 0x98, 0x6e, 0xf8, 0xa3, - 0xb7, 0x3a, 0xf8, 0x96, 0x9c, 0xd4, 0xdb, 0x90, 0x49, 0x14, 0x0e, 0xff, 0x34, 0xd0, 0x28, 0xd9, - 0x18, 0xf8, 0x2f, 0x13, 0xbf, 0x22, 0x3c, 0x3a, 0xf9, 0xbc, 0x38, 0x13, 0x4d, 0x01, 0x8e, 0x20, - 0x13, 0x3c, 0x84, 0x1e, 0xc8, 0xca, 0x3c, 0x40, 0x31, 0x4a, 0xf7, 0x39, 0x5e, 0x43, 0xb3, 0x9c, - 0x8c, 0xf1, 0x7d, 0x37, 0x3f, 0xd8, 0x89, 0x51, 0x7a, 0x8f, 0xfb, 0x82, 0xbc, 0xc6, 0x87, 0x95, - 0x30, 0xb6, 0x37, 0x95, 0x75, 0x51, 0x06, 0xbb, 0x31, 0x4a, 0x87, 0xd3, 0x90, 0xfa, 0x9c, 0xe9, - 0x3a, 0x67, 0xfa, 0x6e, 0x9d, 0xf3, 0xf1, 0xde, 0xf5, 0xf7, 0x09, 0xba, 0xfa, 0x31, 0x41, 0xfc, - 0xa0, 0xfb, 0xda, 0xbb, 0xea, 0xe8, 0xe4, 0x13, 0x7e, 0x74, 0x67, 0x03, 0xf2, 0x04, 0xef, 0xf7, - 0x09, 0xf4, 0xce, 0x46, 0x7c, 0xcf, 0x03, 0xb3, 0x9c, 0x9c, 0xe0, 0x87, 0xbf, 0x8d, 0x3b, 0x4f, - 0x26, 0xd8, 0x71, 0xc9, 0x1c, 0x6d, 0x25, 0xb3, 0xb1, 0x2f, 0x3f, 0x80, 0xdb, 0xa5, 0x99, 0x7e, - 0xc1, 0x87, 0xee, 0xf4, 0x12, 0x20, 0x7f, 0x0b, 0xfa, 0xa2, 0x5b, 0xee, 0x1c, 0x93, 0xbb, 0xf7, - 0x42, 0xd2, 0xad, 0xbe, 0x7f, 0xfd, 0x57, 0xc2, 0x67, 0xff, 0xa0, 0xf4, 0x97, 0x9c, 0x0c, 0x8e, - 0x67, 0xd7, 0xcb, 0x08, 0xdd, 0x2c, 0x23, 0xf4, 0x73, 0x19, 0xa1, 0xab, 0x55, 0x34, 0xb8, 0x59, - 0x45, 0x83, 0x6f, 0xab, 0x68, 0xf0, 0x81, 0x15, 0xa5, 0x3d, 0x6b, 0xe7, 0x74, 0x21, 0x6b, 0x66, - 0xa1, 0xaa, 0xa4, 0x7e, 0x5e, 0x4a, 0xb6, 0xf9, 0x4e, 0x0c, 0xe8, 0x0b, 0xd0, 0xcc, 0x5e, 0x2a, - 0x30, 0xf3, 0x07, 0x2e, 0xf2, 0x17, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x43, 0x3e, 0x3a, 0x18, - 0x4c, 0x03, 0x00, 0x00, + // 432 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0x41, 0x6f, 0xd3, 0x30, + 0x14, 0xc7, 0xeb, 0x0d, 0xd0, 0xe6, 0xaa, 0x63, 0x58, 0x3d, 0x84, 0x50, 0x92, 0x28, 0xa7, 0x70, + 0xc0, 0x96, 0xca, 0x37, 0x98, 0x34, 0xa4, 0x1e, 0x40, 0xc8, 0xc0, 0x85, 0x4b, 0xe4, 0x36, 0x6f, + 0x59, 0xb4, 0x24, 0x0e, 0xb6, 0x33, 0xd8, 0x89, 0xaf, 0xb0, 0x13, 0x9f, 0x69, 0xc7, 0x1d, 0x39, + 0x01, 0x6a, 0xbf, 0x08, 0x8a, 0xe3, 0xa2, 0xb5, 0x05, 0x69, 0x37, 0xbf, 0xff, 0xff, 0xe5, 0xe5, + 0xf7, 0xfe, 0x36, 0x7e, 0x5e, 0x8a, 0x2b, 0x50, 0x2c, 0x13, 0x50, 0xc9, 0x5a, 0xb3, 0x46, 0x15, + 0x0b, 0x38, 0x03, 0xc8, 0x68, 0xa3, 0xa4, 0x91, 0x64, 0x64, 0x6d, 0xea, 0x6c, 0x7f, 0x9c, 0xcb, + 0x5c, 0x5a, 0x87, 0x75, 0xa7, 0xbe, 0xc9, 0x9f, 0xe4, 0x52, 0xe6, 0x25, 0x30, 0xd1, 0x14, 0x4c, + 0xd4, 0xb5, 0x34, 0xc2, 0x14, 0xb2, 0xd6, 0xce, 0x0d, 0x9d, 0x6b, 0xab, 0x79, 0x7b, 0xc6, 0x4c, + 0x51, 0x81, 0x36, 0xa2, 0x6a, 0xfa, 0x86, 0x58, 0xe2, 0xa7, 0x1f, 0x9b, 0x4c, 0x18, 0x78, 0x23, + 0xd4, 0x05, 0x98, 0x77, 0x1d, 0x82, 0xe6, 0xf0, 0xb9, 0x05, 0x6d, 0x08, 0xc7, 0xe3, 0xca, 0xca, + 0xa9, 0x45, 0x4b, 0x5b, 0xdb, 0xa9, 0x3d, 0x14, 0xed, 0x27, 0xc3, 0x69, 0x44, 0x37, 0xf8, 0xe8, + 0x9d, 0x09, 0xfd, 0x48, 0x4e, 0xaa, 0x6d, 0x49, 0xc7, 0x13, 0xec, 0xff, 0xeb, 0x87, 0xba, 0x91, + 0xb5, 0x86, 0xf8, 0x3b, 0xc2, 0xa3, 0xd3, 0xaf, 0x8b, 0x73, 0x51, 0xe7, 0x60, 0x2d, 0x12, 0xe2, + 0x21, 0x38, 0x21, 0x2d, 0x32, 0x0f, 0x45, 0x28, 0x39, 0xe4, 0x78, 0x2d, 0xcd, 0x32, 0x32, 0xc6, + 0x0f, 0x2d, 0x9d, 0xb7, 0x17, 0xa1, 0xe4, 0x01, 0xef, 0x0b, 0xf2, 0x16, 0x1f, 0x97, 0x42, 0x1b, + 0x87, 0x9c, 0x76, 0x6b, 0x7b, 0xfb, 0x11, 0x4a, 0x86, 0x53, 0x9f, 0xf6, 0x99, 0xd0, 0x75, 0x26, + 0xf4, 0xc3, 0x3a, 0x93, 0x93, 0x83, 0x9b, 0x9f, 0x21, 0xba, 0xfe, 0x15, 0x22, 0x7e, 0xd4, 0x7d, + 0xdd, 0x83, 0x76, 0x76, 0xfc, 0x05, 0x3f, 0xd9, 0xd9, 0x8f, 0x3c, 0xc3, 0x87, 0x2e, 0x1f, 0x47, + 0x36, 0xe2, 0x07, 0xbd, 0x30, 0xcb, 0xc8, 0x29, 0x7e, 0xfc, 0x17, 0xdc, 0x32, 0x69, 0x6f, 0xcf, + 0xe6, 0x36, 0xd9, 0xca, 0x6d, 0x63, 0x5f, 0x7e, 0x04, 0x77, 0x4b, 0x3d, 0xfd, 0x86, 0x8f, 0xed, + 0xe9, 0x35, 0x40, 0xf6, 0x1e, 0xd4, 0x65, 0xb7, 0xdc, 0x05, 0x26, 0xbb, 0x19, 0x92, 0x64, 0x6b, + 0xee, 0x7f, 0xef, 0xd5, 0x7f, 0x71, 0x8f, 0x4e, 0x77, 0x21, 0x83, 0x93, 0xd9, 0xcd, 0x32, 0x40, + 0xb7, 0xcb, 0x00, 0xfd, 0x5e, 0x06, 0xe8, 0x7a, 0x15, 0x0c, 0x6e, 0x57, 0xc1, 0xe0, 0xc7, 0x2a, + 0x18, 0x7c, 0x62, 0x79, 0x61, 0xce, 0xdb, 0x39, 0x5d, 0xc8, 0x8a, 0x19, 0x28, 0x4b, 0xa9, 0x5e, + 0x16, 0x92, 0x6d, 0xbe, 0x69, 0x0d, 0xea, 0x12, 0x14, 0x33, 0x57, 0x0d, 0xe8, 0xf9, 0x23, 0x1b, + 0xf9, 0xab, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x87, 0xd2, 0x0f, 0x50, 0xf8, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -417,20 +408,6 @@ func (m *UpdateMarketPricesResponse) MarshalToSizedBuffer(dAtA []byte) (int, err _ = i var l int _ = l - if len(m.MarketPriceUpdates) > 0 { - for iNdEx := len(m.MarketPriceUpdates) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.MarketPriceUpdates[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintPricefeed(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0xa - } - } return len(dAtA) - i, nil } @@ -553,12 +530,6 @@ func (m *UpdateMarketPricesResponse) Size() (n int) { } var l int _ = l - if len(m.MarketPriceUpdates) > 0 { - for _, e := range m.MarketPriceUpdates { - l = e.Size() - n += 1 + l + sovPricefeed(uint64(l)) - } - } return n } @@ -719,40 +690,6 @@ func (m *UpdateMarketPricesResponse) Unmarshal(dAtA []byte) error { return fmt.Errorf("proto: UpdateMarketPricesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field MarketPriceUpdates", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPricefeed - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthPricefeed - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthPricefeed - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.MarketPriceUpdates = append(m.MarketPriceUpdates, &MarketPriceUpdate{}) - if err := m.MarketPriceUpdates[len(m.MarketPriceUpdates)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipPricefeed(dAtA[iNdEx:]) diff --git a/mocks/ExchangeQueryHandler.go b/mocks/ExchangeQueryHandler.go new file mode 100644 index 000000000..5c3b0ef4f --- /dev/null +++ b/mocks/ExchangeQueryHandler.go @@ -0,0 +1,81 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + daemonstypes "github.com/tellor-io/layer/daemons/types" + + mock "github.com/stretchr/testify/mock" + + time "time" + + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// ExchangeQueryHandler is an autogenerated mock type for the ExchangeQueryHandler type +type ExchangeQueryHandler struct { + mock.Mock +} + +// Now provides a mock function with given fields: +func (_m *ExchangeQueryHandler) Now() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// Query provides a mock function with given fields: ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent +func (_m *ExchangeQueryHandler) Query(ctx context.Context, exchangeQueryDetails *types.ExchangeQueryDetails, exchangeConfig *types.MutableExchangeMarketConfig, marketIds []uint32, requestHandler daemonstypes.RequestHandler, marketPriceExponent map[uint32]int32) ([]*types.MarketPriceTimestamp, map[uint32]error, error) { + ret := _m.Called(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + + var r0 []*types.MarketPriceTimestamp + if rf, ok := ret.Get(0).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) []*types.MarketPriceTimestamp); ok { + r0 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.MarketPriceTimestamp) + } + } + + var r1 map[uint32]error + if rf, ok := ret.Get(1).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) map[uint32]error); ok { + r1 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(map[uint32]error) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) error); ok { + r2 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +type mockConstructorTestingTNewExchangeQueryHandler interface { + mock.TestingT + Cleanup(func()) +} + +// NewExchangeQueryHandler creates a new instance of ExchangeQueryHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExchangeQueryHandler(t mockConstructorTestingTNewExchangeQueryHandler) *ExchangeQueryHandler { + mock := &ExchangeQueryHandler{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/RequestHandler.go b/mocks/RequestHandler.go new file mode 100644 index 000000000..6996f58d2 --- /dev/null +++ b/mocks/RequestHandler.go @@ -0,0 +1,53 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// RequestHandler is an autogenerated mock type for the RequestHandler type +type RequestHandler struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, url +func (_m *RequestHandler) Get(ctx context.Context, url string) (*http.Response, error) { + ret := _m.Called(ctx, url) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(context.Context, string) *http.Response); ok { + r0 = rf(ctx, url) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, url) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewRequestHandler interface { + mock.TestingT + Cleanup(func()) +} + +// NewRequestHandler creates a new instance of RequestHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRequestHandler(t mockConstructorTestingTNewRequestHandler) *RequestHandler { + mock := &RequestHandler{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/TimeProvider.go b/mocks/TimeProvider.go new file mode 100644 index 000000000..c5db9b121 --- /dev/null +++ b/mocks/TimeProvider.go @@ -0,0 +1,43 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" +) + +// TimeProvider is an autogenerated mock type for the TimeProvider type +type TimeProvider struct { + mock.Mock +} + +// Now provides a mock function with given fields: +func (_m *TimeProvider) Now() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +type mockConstructorTestingTNewTimeProvider interface { + mock.TestingT + Cleanup(func()) +} + +// NewTimeProvider creates a new instance of TimeProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewTimeProvider(t mockConstructorTestingTNewTimeProvider) *TimeProvider { + mock := &TimeProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/proto/layer/daemons/pricefeed.proto b/proto/layer/daemons/pricefeed.proto index 6041e4a24..7dfc6f89f 100644 --- a/proto/layer/daemons/pricefeed.proto +++ b/proto/layer/daemons/pricefeed.proto @@ -1,4 +1,5 @@ syntax = "proto3"; + package layer.daemons; import "gogoproto/gogo.proto"; @@ -19,16 +20,16 @@ message UpdateMarketPricesRequest { } // UpdateMarketPricesResponse is a response message for updating market prices. -message UpdateMarketPricesResponse { - repeated MarketPriceUpdate market_price_updates = 1; -} +message UpdateMarketPricesResponse {} -// ExchangePrice represents a specific exchange's market price +// ExchangePrice represents a specific exchange's market price message ExchangePrice { string exchange_id = 1; uint64 price = 2; - google.protobuf.Timestamp last_update_time = 3 - [ (gogoproto.nullable) = true, (gogoproto.stdtime) = true ]; + google.protobuf.Timestamp last_update_time = 3 [ + (gogoproto.nullable) = true, + (gogoproto.stdtime) = true + ]; } // MarketPriceUpdate represents an update to a single market diff --git a/testutil/json/json_utils.go b/testutil/json/json_utils.go new file mode 100644 index 000000000..a034e219a --- /dev/null +++ b/testutil/json/json_utils.go @@ -0,0 +1,19 @@ +package json + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// CompactJsonString takes a string of human-readable, formatted JSON and compacts it. +// The purpose is to remove the formatting (e.g. newlines, tabs, etc) and return a string that would match an +// unmarshaled object string generated by a Go program natively. +func CompactJsonString(t *testing.T, jsonString string) string { + buffer := &bytes.Buffer{} + err := json.Compact(buffer, []byte(jsonString)) + require.NoError(t, err, "Error compacting JSON string") + return buffer.String() +} From 8e8b814bc14b846f58ec2030d7c86f9bff551ea5 Mon Sep 17 00:00:00 2001 From: akrem Date: Mon, 22 Jan 2024 09:56:57 -0500 Subject: [PATCH 07/10] remove print and put default_overrides --- app/app.go | 4 +- app/default_overrides.go | 122 ++++++++++++------------ x/oracle/client/cli/query_get_median.go | 2 - 3 files changed, 63 insertions(+), 65 deletions(-) diff --git a/app/app.go b/app/app.go index 3f8b2dd13..a3d08e275 100644 --- a/app/app.go +++ b/app/app.go @@ -184,9 +184,9 @@ var ( auth.AppModuleBasic{}, authzmodule.AppModuleBasic{}, genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), - bank.AppModuleBasic{}, + bankModule{}, capability.AppModuleBasic{}, - staking.AppModuleBasic{}, + stakingModule{}, mint.AppModuleBasic{}, distr.AppModuleBasic{}, gov.NewAppModuleBasic(getGovProposalHandlers()), diff --git a/app/default_overrides.go b/app/default_overrides.go index 1a8a792d5..cdde0a76c 100644 --- a/app/default_overrides.go +++ b/app/default_overrides.go @@ -1,77 +1,77 @@ package app -// import ( -// "encoding/json" +import ( + "encoding/json" -// "cosmossdk.io/math" + "cosmossdk.io/math" -// "github.com/cosmos/cosmos-sdk/codec" -// "github.com/cosmos/cosmos-sdk/x/bank" -// banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" -// distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/bank" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" -// "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/distribution" -// "github.com/cosmos/cosmos-sdk/x/staking" -// stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" -// ) + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) -// // bankModule defines a custom wrapper around the x/bank module's AppModuleBasic -// // implementation to provide custom default genesis state. -// type bankModule struct { -// bank.AppModuleBasic -// } +// bankModule defines a custom wrapper around the x/bank module's AppModuleBasic +// implementation to provide custom default genesis state. +type bankModule struct { + bank.AppModuleBasic +} -// // DefaultGenesis returns custom x/bank module genesis state. -// func (bankModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { -// metadata := banktypes.Metadata{ -// Description: "The native token of the Tellor Layer.", -// Base: BondDenom, -// Name: DisplayDenom, -// Display: DisplayDenom, -// Symbol: DisplayDenom, -// DenomUnits: []*banktypes.DenomUnit{ -// { -// Denom: BondDenom, // ie Wei -// Exponent: 0, -// }, -// { -// Denom: DisplayDenom, //ie Ether -// Exponent: 6, -// }, -// }, -// } +// DefaultGenesis returns custom x/bank module genesis state. +func (bankModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + metadata := banktypes.Metadata{ + Description: "The native token of the Tellor Layer.", + Base: BondDenom, + Name: DisplayDenom, + Display: DisplayDenom, + Symbol: DisplayDenom, + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: BondDenom, // ie Wei + Exponent: 0, + }, + { + Denom: DisplayDenom, //ie Ether + Exponent: 6, + }, + }, + } -// genState := banktypes.DefaultGenesisState() -// genState.DenomMetadata = append(genState.DenomMetadata, metadata) + genState := banktypes.DefaultGenesisState() + genState.DenomMetadata = append(genState.DenomMetadata, metadata) -// return cdc.MustMarshalJSON(genState) -// } + return cdc.MustMarshalJSON(genState) +} -// // stakingModule wraps the x/staking module in order to overwrite specific -// // ModuleManager APIs. -// type stakingModule struct { -// staking.AppModuleBasic -// } +// stakingModule wraps the x/staking module in order to overwrite specific +// ModuleManager APIs. +type stakingModule struct { + staking.AppModuleBasic +} -// // DefaultGenesis returns custom x/staking module genesis state. -// func (stakingModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { -// params := stakingtypes.DefaultParams() -// params.BondDenom = BondDenom +// DefaultGenesis returns custom x/staking module genesis state. +func (stakingModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + params := stakingtypes.DefaultParams() + params.BondDenom = BondDenom -// return cdc.MustMarshalJSON(&stakingtypes.GenesisState{ -// Params: params, -// }) -// } + return cdc.MustMarshalJSON(&stakingtypes.GenesisState{ + Params: params, + }) +} -// type distrModule struct { -// distribution.AppModuleBasic -// } +type distrModule struct { + distribution.AppModuleBasic +} -// // DefaultGenesis returns custom x/distribution module genesis state. -// func (distrModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { -// genState := distrtypes.DefaultGenesisState() -// genState.Params.CommunityTax = math.LegacyZeroDec() // 0% community tax on gas fees, inflation is minted to timeBasedRewards for reporters +// DefaultGenesis returns custom x/distribution module genesis state. +func (distrModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + genState := distrtypes.DefaultGenesisState() + genState.Params.CommunityTax = math.LegacyZeroDec() // 0% community tax on gas fees, inflation is minted to timeBasedRewards for reporters -// return cdc.MustMarshalJSON(genState) -// } + return cdc.MustMarshalJSON(genState) +} diff --git a/x/oracle/client/cli/query_get_median.go b/x/oracle/client/cli/query_get_median.go index 1d8bb7e7b..b4d0c563f 100644 --- a/x/oracle/client/cli/query_get_median.go +++ b/x/oracle/client/cli/query_get_median.go @@ -1,7 +1,6 @@ package cli import ( - "fmt" "strconv" "github.com/cosmos/cosmos-sdk/client" @@ -23,7 +22,6 @@ func CmdGetMedianValues() *cobra.Command { if err != nil { return err } - fmt.Println(clientCtx.Height) queryClient := mediantypes.NewMedianValuesServiceClient(clientCtx) From 67a4e8dda42c76811610fc79f836048eda8064b7 Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 24 Jan 2024 12:18:26 -0500 Subject: [PATCH 08/10] Add query data mapping to market params + cleanup --- app/app.go | 12 +- app/flags/flags.go | 78 +- cmd/layerd/cmd/root_option.go | 3 - .../configs/default_market_param_config.go | 10 +- .../constants/static_market_params_config.go | 2 + daemons/pricefeed/client/client.go | 8 + .../pricefeed/client/types/market_param.go | 12 + daemons/readme.md | 29 +- daemons/server/median/median.go | 45 +- daemons/server/types/median_values.pb.go | 602 ++- daemons/server/types/median_values.pb.gw.go | 129 +- docs/static/openapi.yml | 4653 +++++++---------- lib/prices/utils.go | 39 + proto/layer/daemons/median_values.proto | 33 +- x/oracle/client/cli/query.go | 4 +- x/oracle/client/cli/query_all_get_median.go | 42 + x/oracle/client/cli/query_get_median.go | 13 +- .../client/cli/tx_commit_submit_report.go | 65 +- 18 files changed, 2624 insertions(+), 3155 deletions(-) create mode 100644 x/oracle/client/cli/query_all_get_median.go diff --git a/app/app.go b/app/app.go index a3d08e275..d3b474ef4 100644 --- a/app/app.go +++ b/app/app.go @@ -627,17 +627,18 @@ func New( app.Logger(), daemonFlags.Shared.PanicOnDaemonFailureEnabled, ) - // Create a closure for starting daemons and daemon server. Daemon services are delayed until after the gRPC + // Create a closure for starting pricefeed daemon and daemon server. Daemon services are delayed until after the gRPC // service is started because daemons depend on the gRPC service being available. If a node is initialized // with a genesis time in the future, then the gRPC service will not be available until the genesis time, the // daemons will not be able to connect to the cosmos gRPC query service and finish initialization, and the daemon // monitoring service will panic. app.startDaemons = func(cltx client.Context, apiSvr *api.Server) { - maxDaemonUnhealthyDuration := time.Duration(daemonFlags.Shared.MaxDaemonUnhealthySeconds) * time.Second - // Start server for handling gRPC messages from daemons. - go app.Server.Start() - + // enabled by default, set flag `--price-daemon-enabled=false` to false to disable if daemonFlags.Price.Enabled { + maxDaemonUnhealthyDuration := time.Second + // Start server for handling gRPC messages from daemons. + go app.Server.Start() + exchangeQueryConfig := configs.ReadExchangeQueryConfigFile(homePath) marketParamsConfig := configs.ReadMarketParamsConfigFile(homePath) // Start pricefeed client for sending prices for the pricefeed server to consume. These prices @@ -661,7 +662,6 @@ func New( } // Start the Metrics Daemon. // The metrics daemon is purely used for observability. It should never bring the app down. - // TODO(CLOB-960) Don't start this goroutine if telemetry is disabled // Note: the metrics daemon is such a simple go-routine that we don't bother implementing a health-check // for this service. The task loop does not produce any errors because the telemetry calls themselves are // not error-returning, so in effect this daemon would never become unhealthy. diff --git a/app/flags/flags.go b/app/flags/flags.go index 744293159..2d5f0cd5b 100644 --- a/app/flags/flags.go +++ b/app/flags/flags.go @@ -6,16 +6,10 @@ import ( "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/spf13/cast" - "github.com/spf13/cobra" ) // A struct containing the values of all flags. type Flags struct { - DdAgentHost string - DdTraceAgentPort uint16 - NonValidatingFullNode bool - DdErrorTrackingFormat bool - // Existing flags GrpcAddress string GrpcEnable bool @@ -23,55 +17,15 @@ type Flags struct { // List of CLI flags. const ( - DdAgentHost = "dd-agent-host" - DdTraceAgentPort = "dd-trace-agent-port" - NonValidatingFullNodeFlag = "non-validating-full-node" - DdErrorTrackingFormat = "dd-error-tracking-format" - // Cosmos flags below. These config values can be set as flags or in config.toml. GrpcAddress = "grpc.address" GrpcEnable = "grpc.enable" ) -// Default values. -const ( - DefaultDdAgentHost = "" - DefaultDdTraceAgentPort = 8126 - DefaultNonValidatingFullNode = false - DefaultDdErrorTrackingFormat = false -) - -// AddFlagsToCmd adds flags to app initialization. -// These flags should be applied to the `start` command of the LAYER Cosmos application. -// E.g. `layerd start --non-validating-full-node true`. -func AddFlagsToCmd(cmd *cobra.Command) { - cmd.Flags().Bool( - NonValidatingFullNodeFlag, - DefaultNonValidatingFullNode, - "Whether to run in non-validating full-node mode. "+ - "This disables the pricing daemon.", - ) - cmd.Flags().String( - DdAgentHost, - DefaultDdAgentHost, - "Sets the address to connect to for the Datadog Agent.", - ) - cmd.Flags().Uint16( - DdTraceAgentPort, - DefaultDdTraceAgentPort, - "Sets the Datadog Agent port.", - ) - cmd.Flags().Bool( - DdErrorTrackingFormat, - DefaultDdErrorTrackingFormat, - "Enable formatting of log error tags to datadog error tracking format", - ) -} - // Validate checks that the flags are valid. func (f *Flags) Validate() error { // Validtors must have cosmos grpc services enabled. - if !f.NonValidatingFullNode && !f.GrpcEnable { + if !f.GrpcEnable { return fmt.Errorf("grpc.enable must be set to true - validating requires gRPC server") } return nil @@ -84,41 +38,11 @@ func GetFlagValuesFromOptions( ) Flags { // Create default result. result := Flags{ - NonValidatingFullNode: DefaultNonValidatingFullNode, - DdAgentHost: DefaultDdAgentHost, - DdTraceAgentPort: DefaultDdTraceAgentPort, - DdErrorTrackingFormat: DefaultDdErrorTrackingFormat, - // These are the default values from the Cosmos flags. GrpcAddress: config.DefaultGRPCAddress, GrpcEnable: true, } - // Populate the flags if they exist. - if option := appOpts.Get(NonValidatingFullNodeFlag); option != nil { - if v, err := cast.ToBoolE(option); err == nil { - result.NonValidatingFullNode = v - } - } - - if option := appOpts.Get(DdAgentHost); option != nil { - if v, err := cast.ToStringE(option); err == nil { - result.DdAgentHost = v - } - } - - if option := appOpts.Get(DdTraceAgentPort); option != nil { - if v, err := cast.ToUint16E(option); err == nil { - result.DdTraceAgentPort = v - } - } - - if option := appOpts.Get(DdErrorTrackingFormat); option != nil { - if v, err := cast.ToBoolE(option); err == nil { - result.DdErrorTrackingFormat = v - } - } - if option := appOpts.Get(GrpcAddress); option != nil { if v, err := cast.ToStringE(option); err == nil { result.GrpcAddress = v diff --git a/cmd/layerd/cmd/root_option.go b/cmd/layerd/cmd/root_option.go index e2c084cc5..5d02e6742 100644 --- a/cmd/layerd/cmd/root_option.go +++ b/cmd/layerd/cmd/root_option.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/spf13/cobra" - appflags "github.com/tellor-io/layer/app/flags" daemonflags "github.com/tellor-io/layer/daemons/flags" ) @@ -25,8 +24,6 @@ func (o *RootCmdOption) setCustomizeStartCmd(f func(startCmd *cobra.Command)) { func GetOptionWithCustomStartCmd() *RootCmdOption { option := newRootCmdOption() f := func(cmd *cobra.Command) { - // Add app flags. - appflags.AddFlagsToCmd(cmd) // Add daemon flags. daemonflags.AddDaemonFlagsToCmd(cmd) diff --git a/daemons/configs/default_market_param_config.go b/daemons/configs/default_market_param_config.go index 893fb0967..726fa156c 100644 --- a/daemons/configs/default_market_param_config.go +++ b/daemons/configs/default_market_param_config.go @@ -29,9 +29,12 @@ const ( # # Exponent - The exponent of the price. # - # Pair - The human-readable name of the market pair (e.g. "BTC-USD").{{ range $exchangeId, $element := .}} + # Pair - The human-readable name of the market pair (e.g. "BTC-USD"). + # + # QueryData - Layer representation of the market pair.{{ range $exchangeId, $element := .}} [[market_params]] ExchangeConfigJson = "{{$element.ExchangeConfigJson}}" + QueryData = {{$element.QueryData}} Exponent = {{$element.Exponent}} Id = {{$element.Id}} MinExchanges = {{$element.MinExchanges}} @@ -75,6 +78,7 @@ func ReadMarketParamsConfigFile(homeDir string) []types.MarketParam { params := map[string][]types.MarketParam{} if err = toml.Unmarshal(tomlFile, ¶ms); err != nil { + fmt.Println("Error unmarshalling toml file", err.Error()) panic(err) } @@ -82,7 +86,8 @@ func ReadMarketParamsConfigFile(homeDir string) []types.MarketParam { for _, param := range params["market_params"] { if param.Exponent == 0 || param.MinExchanges == 0 || - param.MinPriceChangePpm == 0 { + param.MinPriceChangePpm == 0 || + param.QueryData == "" { panic( fmt.Errorf( "One or more config values are unset or are set to zero for pair with id: '%v'", @@ -99,6 +104,7 @@ func ReadMarketParamsConfigFile(homeDir string) []types.MarketParam { MinExchanges: param.MinExchanges, MinPriceChangePpm: param.MinPriceChangePpm, Pair: param.Pair, + QueryData: param.QueryData, } } marketParams := make([]types.MarketParam, 0, len(paramStartupConfigMap)) diff --git a/daemons/constants/static_market_params_config.go b/daemons/constants/static_market_params_config.go index 6ff4b4905..ac269dece 100644 --- a/daemons/constants/static_market_params_config.go +++ b/daemons/constants/static_market_params_config.go @@ -14,6 +14,7 @@ var ( MinExchanges: 1, MinPriceChangePpm: 1000, ExchangeConfigJson: `{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"BTCUSDT\\\"\"},{\"exchangeName\":\"BinanceUS\",\"ticker\":\"\\\"BTCUSD\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tBTCUSD\"},{\"exchangeName\":\"Bitstamp\",\"ticker\":\"BTC/USD\"},{\"exchangeName\":\"Bybit\",\"ticker\":\"BTCUSDT\"},{\"exchangeName\":\"CoinbasePro\",\"ticker\":\"BTC-USD\"},{\"exchangeName\":\"CryptoCom\",\"ticker\":\"BTC_USD\"},{\"exchangeName\":\"Kraken\",\"ticker\":\"XXBTZUSD\"},{\"exchangeName\":\"Okx\",\"ticker\":\"BTC-USDT\"}]}`, + QueryData: `"00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003627463000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000"`, }, exchange_common.ETHUSD_ID: { Id: exchange_common.ETHUSD_ID, @@ -22,6 +23,7 @@ var ( MinExchanges: 1, MinPriceChangePpm: 1000, ExchangeConfigJson: `{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHUSDT\\\"\"},{\"exchangeName\":\"BinanceUS\",\"ticker\":\"\\\"ETHUSD\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHUSD\"},{\"exchangeName\":\"Bitstamp\",\"ticker\":\"ETH/USD\"},{\"exchangeName\":\"Bybit\",\"ticker\":\"ETHUSDT\"},{\"exchangeName\":\"CoinbasePro\",\"ticker\":\"ETH-USD\"},{\"exchangeName\":\"CryptoCom\",\"ticker\":\"ETH_USD\"},{\"exchangeName\":\"Kraken\",\"ticker\":\"XETHZUSD\"},{\"exchangeName\":\"Okx\",\"ticker\":\"ETH-USDT\"}]}`, + QueryData: `"00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003657468000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000"`, }, } ) diff --git a/daemons/pricefeed/client/client.go b/daemons/pricefeed/client/client.go index f05bd1de7..b9605e2a2 100644 --- a/daemons/pricefeed/client/client.go +++ b/daemons/pricefeed/client/client.go @@ -163,6 +163,7 @@ func (c *Client) start(ctx context.Context, // 2. Validate daemon configuration. if err := validateDaemonConfiguration( + marketParams, exchangeIdToQueryConfig, exchangeIdToExchangeDetails, ); err != nil { @@ -304,6 +305,7 @@ func StartNewClient( // 1) The exchangeIdToExchangeDetails map has an entry for each exchange. // 2) The static exchange names map has an entry for each exchange, and each name is unique. func validateDaemonConfiguration( + marketParams []types.MarketParam, exchangeIdToQueryConfig map[types.ExchangeId]*types.ExchangeQueryConfig, exchangeIdToExchangeDetails map[types.ExchangeId]types.ExchangeQueryDetails, ) ( @@ -329,5 +331,11 @@ func validateDaemonConfiguration( return errors.New("exchangeIds must not be empty") } + for mp := range marketParams { + if err := marketParams[mp].Validate(); err != nil { + return err + } + } + return nil } diff --git a/daemons/pricefeed/client/types/market_param.go b/daemons/pricefeed/client/types/market_param.go index 48d2bb027..ce3f1cc5f 100644 --- a/daemons/pricefeed/client/types/market_param.go +++ b/daemons/pricefeed/client/types/market_param.go @@ -1,6 +1,7 @@ package types import ( + "encoding/hex" "encoding/json" "fmt" ) @@ -28,6 +29,8 @@ type MarketParam struct { // A string of json that encodes the configuration for resolving the price // of this market on various exchanges. ExchangeConfigJson string + // query data representation of the market for layer + QueryData string } // Validate checks that the MarketParam is valid. @@ -56,6 +59,15 @@ func (mp *MarketParam) Validate() error { ) } + if mp.QueryData == "" { + return fmt.Errorf("Invalid input: QueryData cannot be empty") + } + // try to decode query data from hex to bytes if this fails then return error + _, err := hex.DecodeString(mp.QueryData) + if err != nil { + return fmt.Errorf("Invalid input: QueryData is not valid hex: %v", err) + } + return nil } diff --git a/daemons/readme.md b/daemons/readme.md index 4d992e49c..8bcf99b03 100644 --- a/daemons/readme.md +++ b/daemons/readme.md @@ -1,5 +1,7 @@ # Daemon gRPC Server +**Note:** Daemon services code was adopted from dydx [](https://github.com/dydxprotocol/v4-chain/tree/main/protocol/daemons) and reconfigured. + ## Overview Implements a gRPC server for daemon processes using Unix Domain Sockets (UDS) for communication. @@ -43,20 +45,23 @@ MaxQueries = 1 // Max number of calls in a loop. Defaults for exchange information can be found [here](./configs/default_pricefeed_exchange_config.go) -## Market Pair defauls +## Market Pair defaults + +Defaults for market pair can be found [here](./configs/default_market_param_config.go) + +example: ```go [[market_params]] -ExchangeConfigJson = "{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHBTC\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHBTC\",\"adjustByMarket\":\"BTC-USD\"}]}" +ExchangeConfigJson = "{\"exchanges\":[{\"exchangeName\":\"Binance\",\"ticker\":\"\\\"ETHBTC\\\"\"},{\"exchangeName\":\"Bitfinex\",\"ticker\":\"tETHBTC\",\"adjustByMarket\":\"BTC-USD\"}]}" // this is just an example to show how to use adjustByMarket. you can use ETH-USD without adjustbymarket Exponent = -6 Id = 2 MinExchanges = 1 MinPriceChangePpm = 1000 Pair = "ETH-BTC" +QueryData = "0000.." ``` -Defaults for market pair can be found [here](./configs/default_market_param_config.go) - ```go type MarketParam struct { // Unique, sequentially-generated value. @@ -77,8 +82,20 @@ type MarketParam struct { // A string of json that encodes the configuration for resolving the price // of this market on various exchanges. ExchangeConfigJson string + // Query data is the market pair represention in layer + QueryData string } ``` -Note: Price Daemon is enabled by default to disable set `--price-daemon-enabled=false` -TODO: Add mapping of queryData to Id +**Note:** Price Daemon is enabled by default; to disable set `--price-daemon-enabled=false` +A price is valid by default up to 30 seconds; to change this to a different default edit the `constants.MaxPriceAge` + +**Also:** Config files are written to homedir/.layer/config/. +To change/add exchange details or market pairs edit the files `pricefeed_exchange_config.toml` or `market_params.toml` respectively. + +### Median Server + +Median server was added for a way to query median values that were from an endpoint or cli. See usage [here](../x/oracle/client/cli/query_all_get_median.go). +All median values or median value given query data using the following commands respectively. +`layerd query oracle get-all-median-values` +`layerd query oracle get-median-value ` diff --git a/daemons/server/median/median.go b/daemons/server/median/median.go index 73f09adf4..b32e2c73e 100644 --- a/daemons/server/median/median.go +++ b/daemons/server/median/median.go @@ -2,6 +2,7 @@ package median import ( "context" + "fmt" "time" "github.com/cosmos/cosmos-sdk/client" @@ -15,30 +16,58 @@ import ( var _ types.MedianValuesServiceServer = &medianServer{} type medianServer struct { - clientCtx client.Context - marketToExchange *pricefeedservertypes.MarketToExchangePrices - marketParams []pricefeedtypes.MarketParam + clientCtx client.Context + marketToExchange *pricefeedservertypes.MarketToExchangePrices + marketParams []pricefeedtypes.MarketParam + queryDataTomarketParams map[string]pricefeedtypes.MarketParam } func NewMedianValuesServer(clientCtx client.Context, marketToExchange *pricefeedservertypes.MarketToExchangePrices, marketParams []pricefeedtypes.MarketParam) types.MedianValuesServiceServer { + mapQueryDataToMarketParams := make(map[string]pricefeedtypes.MarketParam) + for _, marketParam := range marketParams { + mapQueryDataToMarketParams[marketParam.QueryData] = marketParam + } return &medianServer{ - clientCtx: clientCtx, - marketToExchange: marketToExchange, - marketParams: marketParams, + clientCtx: clientCtx, + marketToExchange: marketToExchange, + marketParams: marketParams, + queryDataTomarketParams: mapQueryDataToMarketParams, } } -func (s *medianServer) GetMedianValues(ctx context.Context, req *types.GetMedianValuesRequest) (*types.GetMedianValuesResponse, error) { +func (s *medianServer) GetAllMedianValues(ctx context.Context, req *types.GetAllMedianValuesRequest) (*types.GetAllMedianValuesResponse, error) { values := s.marketToExchange.GetValidMedianPrices(s.marketParams, time.Now()) medianValues := make([]*types.MedianValues, 0, len(values)) for i, value := range values { medianValues = append(medianValues, &types.MedianValues{ MarketId: i, Price: value, + // TODO: add exponent as well here + // Exponent: , }) } - return &types.GetMedianValuesResponse{MedianValues: medianValues}, nil + return &types.GetAllMedianValuesResponse{MedianValues: medianValues}, nil + +} +func (s *medianServer) GetMedianValue(ctx context.Context, req *types.GetMedianValueRequest) (*types.GetMedianValueResponse, error) { + // check if query data exists in map + mp, found := s.queryDataTomarketParams[req.QueryData] + if !found { + return nil, fmt.Errorf("no market param found for query data: %s", req.QueryData) + } + mv := s.marketToExchange.GetValidMedianPrices([]pricefeedtypes.MarketParam{mp}, time.Now()) + val, found := mv[mp.Id] + if !found { + return nil, fmt.Errorf("no median values found for query data: %s", req.QueryData) + } + res := types.MedianValues{ + // TODO: should market id be a string(querydata)? + MarketId: mp.Id, + Price: val, + Exponent: mp.Exponent, + } + return &types.GetMedianValueResponse{MedianValues: &res}, nil } func StartMedianServer( diff --git a/daemons/server/types/median_values.pb.go b/daemons/server/types/median_values.pb.go index 37e1fd464..ade85febb 100644 --- a/daemons/server/types/median_values.pb.go +++ b/daemons/server/types/median_values.pb.go @@ -34,6 +34,7 @@ type MedianValues struct { // marketId is the market id for a pair MarketId uint32 `protobuf:"varint,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` Price uint64 `protobuf:"varint,2,opt,name=price,proto3" json:"price,omitempty"` + Exponent int32 `protobuf:"varint,3,opt,name=exponent,proto3" json:"exponent,omitempty"` } func (m *MedianValues) Reset() { *m = MedianValues{} } @@ -83,22 +84,29 @@ func (m *MedianValues) GetPrice() uint64 { return 0 } -// GetMedianValuesRequest is the request for the GetMedianValues rpc -type GetMedianValuesRequest struct { +func (m *MedianValues) GetExponent() int32 { + if m != nil { + return m.Exponent + } + return 0 +} + +// GetAllMedianValuesRequest is the request for the GetAllMedianValues rpc +type GetAllMedianValuesRequest struct { } -func (m *GetMedianValuesRequest) Reset() { *m = GetMedianValuesRequest{} } -func (m *GetMedianValuesRequest) String() string { return proto.CompactTextString(m) } -func (*GetMedianValuesRequest) ProtoMessage() {} -func (*GetMedianValuesRequest) Descriptor() ([]byte, []int) { +func (m *GetAllMedianValuesRequest) Reset() { *m = GetAllMedianValuesRequest{} } +func (m *GetAllMedianValuesRequest) String() string { return proto.CompactTextString(m) } +func (*GetAllMedianValuesRequest) ProtoMessage() {} +func (*GetAllMedianValuesRequest) Descriptor() ([]byte, []int) { return fileDescriptor_9b64c378347d9f4f, []int{1} } -func (m *GetMedianValuesRequest) XXX_Unmarshal(b []byte) error { +func (m *GetAllMedianValuesRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *GetMedianValuesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *GetAllMedianValuesRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_GetMedianValuesRequest.Marshal(b, m, deterministic) + return xxx_messageInfo_GetAllMedianValuesRequest.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -108,35 +116,126 @@ func (m *GetMedianValuesRequest) XXX_Marshal(b []byte, deterministic bool) ([]by return b[:n], nil } } -func (m *GetMedianValuesRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_GetMedianValuesRequest.Merge(m, src) +func (m *GetAllMedianValuesRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetAllMedianValuesRequest.Merge(m, src) } -func (m *GetMedianValuesRequest) XXX_Size() int { +func (m *GetAllMedianValuesRequest) XXX_Size() int { return m.Size() } -func (m *GetMedianValuesRequest) XXX_DiscardUnknown() { - xxx_messageInfo_GetMedianValuesRequest.DiscardUnknown(m) +func (m *GetAllMedianValuesRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetAllMedianValuesRequest.DiscardUnknown(m) } -var xxx_messageInfo_GetMedianValuesRequest proto.InternalMessageInfo +var xxx_messageInfo_GetAllMedianValuesRequest proto.InternalMessageInfo -// GetMedianValuesResponse is the response for the GetMedianValues rpc -type GetMedianValuesResponse struct { +// GetAllMedianValuesResponse is the response for the GetAllMedianValues rpc +type GetAllMedianValuesResponse struct { MedianValues []*MedianValues `protobuf:"bytes,1,rep,name=median_values,json=medianValues,proto3" json:"median_values,omitempty"` } -func (m *GetMedianValuesResponse) Reset() { *m = GetMedianValuesResponse{} } -func (m *GetMedianValuesResponse) String() string { return proto.CompactTextString(m) } -func (*GetMedianValuesResponse) ProtoMessage() {} -func (*GetMedianValuesResponse) Descriptor() ([]byte, []int) { +func (m *GetAllMedianValuesResponse) Reset() { *m = GetAllMedianValuesResponse{} } +func (m *GetAllMedianValuesResponse) String() string { return proto.CompactTextString(m) } +func (*GetAllMedianValuesResponse) ProtoMessage() {} +func (*GetAllMedianValuesResponse) Descriptor() ([]byte, []int) { return fileDescriptor_9b64c378347d9f4f, []int{2} } -func (m *GetMedianValuesResponse) XXX_Unmarshal(b []byte) error { +func (m *GetAllMedianValuesResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GetAllMedianValuesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GetAllMedianValuesResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GetAllMedianValuesResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetAllMedianValuesResponse.Merge(m, src) +} +func (m *GetAllMedianValuesResponse) XXX_Size() int { + return m.Size() +} +func (m *GetAllMedianValuesResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetAllMedianValuesResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetAllMedianValuesResponse proto.InternalMessageInfo + +func (m *GetAllMedianValuesResponse) GetMedianValues() []*MedianValues { + if m != nil { + return m.MedianValues + } + return nil +} + +// GetMedianValuesRequest is the request for the GetMedianValues rpc +type GetMedianValueRequest struct { + // query data to fetch prices for + QueryData string `protobuf:"bytes,1,opt,name=query_data,json=queryData,proto3" json:"query_data,omitempty"` +} + +func (m *GetMedianValueRequest) Reset() { *m = GetMedianValueRequest{} } +func (m *GetMedianValueRequest) String() string { return proto.CompactTextString(m) } +func (*GetMedianValueRequest) ProtoMessage() {} +func (*GetMedianValueRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_9b64c378347d9f4f, []int{3} +} +func (m *GetMedianValueRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GetMedianValueRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GetMedianValueRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GetMedianValueRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetMedianValueRequest.Merge(m, src) +} +func (m *GetMedianValueRequest) XXX_Size() int { + return m.Size() +} +func (m *GetMedianValueRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetMedianValueRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetMedianValueRequest proto.InternalMessageInfo + +func (m *GetMedianValueRequest) GetQueryData() string { + if m != nil { + return m.QueryData + } + return "" +} + +// GetMedianValueResponse is the response for the GetMedianValue rpc +type GetMedianValueResponse struct { + MedianValues *MedianValues `protobuf:"bytes,1,opt,name=median_values,json=medianValues,proto3" json:"median_values,omitempty"` +} + +func (m *GetMedianValueResponse) Reset() { *m = GetMedianValueResponse{} } +func (m *GetMedianValueResponse) String() string { return proto.CompactTextString(m) } +func (*GetMedianValueResponse) ProtoMessage() {} +func (*GetMedianValueResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_9b64c378347d9f4f, []int{4} +} +func (m *GetMedianValueResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *GetMedianValuesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *GetMedianValueResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_GetMedianValuesResponse.Marshal(b, m, deterministic) + return xxx_messageInfo_GetMedianValueResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -146,19 +245,19 @@ func (m *GetMedianValuesResponse) XXX_Marshal(b []byte, deterministic bool) ([]b return b[:n], nil } } -func (m *GetMedianValuesResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_GetMedianValuesResponse.Merge(m, src) +func (m *GetMedianValueResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetMedianValueResponse.Merge(m, src) } -func (m *GetMedianValuesResponse) XXX_Size() int { +func (m *GetMedianValueResponse) XXX_Size() int { return m.Size() } -func (m *GetMedianValuesResponse) XXX_DiscardUnknown() { - xxx_messageInfo_GetMedianValuesResponse.DiscardUnknown(m) +func (m *GetMedianValueResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetMedianValueResponse.DiscardUnknown(m) } -var xxx_messageInfo_GetMedianValuesResponse proto.InternalMessageInfo +var xxx_messageInfo_GetMedianValueResponse proto.InternalMessageInfo -func (m *GetMedianValuesResponse) GetMedianValues() []*MedianValues { +func (m *GetMedianValueResponse) GetMedianValues() *MedianValues { if m != nil { return m.MedianValues } @@ -167,35 +266,44 @@ func (m *GetMedianValuesResponse) GetMedianValues() []*MedianValues { func init() { proto.RegisterType((*MedianValues)(nil), "layer.daemons.MedianValues") - proto.RegisterType((*GetMedianValuesRequest)(nil), "layer.daemons.GetMedianValuesRequest") - proto.RegisterType((*GetMedianValuesResponse)(nil), "layer.daemons.GetMedianValuesResponse") + proto.RegisterType((*GetAllMedianValuesRequest)(nil), "layer.daemons.GetAllMedianValuesRequest") + proto.RegisterType((*GetAllMedianValuesResponse)(nil), "layer.daemons.GetAllMedianValuesResponse") + proto.RegisterType((*GetMedianValueRequest)(nil), "layer.daemons.GetMedianValueRequest") + proto.RegisterType((*GetMedianValueResponse)(nil), "layer.daemons.GetMedianValueResponse") } func init() { proto.RegisterFile("layer/daemons/median_values.proto", fileDescriptor_9b64c378347d9f4f) } var fileDescriptor_9b64c378347d9f4f = []byte{ - // 335 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0x31, 0x4b, 0xfb, 0x40, - 0x18, 0xc6, 0x7b, 0xfd, 0xff, 0x15, 0x3d, 0x5b, 0x84, 0xb3, 0x68, 0x68, 0xcb, 0x51, 0x83, 0x4a, - 0x17, 0x73, 0x50, 0xbf, 0x80, 0xba, 0x48, 0x07, 0x97, 0x0a, 0x0e, 0x3a, 0x94, 0x6b, 0xf3, 0x12, - 0x0f, 0x93, 0xbc, 0xf1, 0xee, 0x5a, 0xe8, 0xea, 0xe0, 0x2c, 0xb8, 0xfb, 0x79, 0x1c, 0x0b, 0x2e, - 0x8e, 0xd2, 0xfa, 0x41, 0xa4, 0x49, 0x87, 0xa4, 0x0a, 0x6e, 0x79, 0xf3, 0x3c, 0xf7, 0xbb, 0xf7, - 0xb9, 0x87, 0xee, 0x87, 0x72, 0x02, 0x5a, 0xf8, 0x12, 0x22, 0x8c, 0x8d, 0x88, 0xc0, 0x57, 0x32, - 0xee, 0x8f, 0x65, 0x38, 0x02, 0xe3, 0x25, 0x1a, 0x2d, 0xb2, 0x6a, 0x6a, 0xf1, 0x96, 0x96, 0x7a, - 0x2d, 0xc0, 0x00, 0x53, 0x45, 0x2c, 0xbe, 0x32, 0x53, 0xbd, 0x19, 0x20, 0x06, 0x21, 0x08, 0x99, - 0x28, 0x21, 0xe3, 0x18, 0xad, 0xb4, 0x0a, 0xe3, 0x25, 0xc2, 0x3d, 0xa3, 0x95, 0xcb, 0x94, 0x7c, - 0x9d, 0x82, 0x59, 0x83, 0x6e, 0x46, 0x52, 0xdf, 0x83, 0xed, 0x2b, 0xdf, 0x21, 0x2d, 0xd2, 0xae, - 0xf6, 0x36, 0xb2, 0x1f, 0x5d, 0x9f, 0xd5, 0xe8, 0x5a, 0xa2, 0xd5, 0x10, 0x9c, 0x72, 0x8b, 0xb4, - 0xff, 0xf7, 0xb2, 0xc1, 0x75, 0xe8, 0xee, 0x05, 0xd8, 0x3c, 0xa5, 0x07, 0x0f, 0x23, 0x30, 0xd6, - 0xbd, 0xa5, 0x7b, 0x3f, 0x14, 0x93, 0x60, 0x6c, 0x80, 0x9d, 0xd2, 0x6a, 0x21, 0x91, 0x43, 0x5a, - 0xff, 0xda, 0x5b, 0x9d, 0x86, 0x57, 0x88, 0xe4, 0x15, 0xce, 0x56, 0xa2, 0xdc, 0xd4, 0x79, 0x25, - 0x74, 0x27, 0x2f, 0x5f, 0x81, 0x1e, 0xab, 0x21, 0xb0, 0x27, 0x42, 0xb7, 0x57, 0x6e, 0x65, 0x87, - 0x2b, 0xd8, 0xdf, 0xf7, 0xad, 0x1f, 0xfd, 0x65, 0xcb, 0x96, 0x77, 0x0f, 0x1e, 0xdf, 0xbf, 0x5e, - 0xca, 0x9c, 0x35, 0x45, 0xd6, 0xd1, 0xe2, 0x69, 0x03, 0xb0, 0xfd, 0x42, 0xa2, 0xf3, 0xee, 0xdb, - 0x8c, 0x93, 0xe9, 0x8c, 0x93, 0xcf, 0x19, 0x27, 0xcf, 0x73, 0x5e, 0x9a, 0xce, 0x79, 0xe9, 0x63, - 0xce, 0x4b, 0x37, 0x22, 0x50, 0xf6, 0x6e, 0x34, 0xf0, 0x86, 0x18, 0x09, 0x0b, 0x61, 0x88, 0xfa, - 0x58, 0xa1, 0x28, 0xf6, 0x6d, 0x40, 0x8f, 0x41, 0x0b, 0x3b, 0x49, 0xc0, 0x0c, 0xd6, 0xd3, 0xb2, - 0x4e, 0xbe, 0x03, 0x00, 0x00, 0xff, 0xff, 0x00, 0x52, 0x3d, 0xbd, 0x14, 0x02, 0x00, 0x00, + // 437 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x41, 0x8b, 0xd3, 0x40, + 0x14, 0xee, 0x74, 0x5d, 0xd9, 0x8e, 0x5b, 0x0f, 0xe3, 0x2a, 0x31, 0xd5, 0x10, 0x83, 0x42, 0x14, + 0xcc, 0xc8, 0x0a, 0x9e, 0x55, 0x84, 0xa5, 0x07, 0x2f, 0x11, 0x3c, 0x14, 0x34, 0x4c, 0x9b, 0x47, + 0x0c, 0x4e, 0x66, 0xd2, 0xc9, 0xa4, 0x58, 0xc4, 0x8b, 0x7f, 0x40, 0xa1, 0xbf, 0xc7, 0xbb, 0xc7, + 0x82, 0x17, 0x8f, 0xd2, 0xfa, 0x43, 0xa4, 0x93, 0xa0, 0x49, 0x6b, 0xd1, 0xbd, 0xe5, 0xcd, 0xfb, + 0xe6, 0x7b, 0xdf, 0xf7, 0x65, 0x1e, 0xbe, 0xc5, 0xd9, 0x1c, 0x14, 0x8d, 0x19, 0x64, 0x52, 0x14, + 0x34, 0x83, 0x38, 0x65, 0x22, 0x9a, 0x31, 0x5e, 0x42, 0x11, 0xe4, 0x4a, 0x6a, 0x49, 0xfa, 0x06, + 0x12, 0xd4, 0x10, 0xfb, 0x24, 0x91, 0x89, 0x34, 0x1d, 0xba, 0xf9, 0xaa, 0x40, 0xf6, 0x8d, 0x44, + 0xca, 0x84, 0x03, 0x65, 0x79, 0x4a, 0x99, 0x10, 0x52, 0x33, 0x9d, 0x4a, 0x51, 0x53, 0x78, 0xaf, + 0xf0, 0xf1, 0x73, 0xc3, 0xfc, 0xd2, 0x10, 0x93, 0x01, 0xee, 0x65, 0x4c, 0xbd, 0x05, 0x1d, 0xa5, + 0xb1, 0x85, 0x5c, 0xe4, 0xf7, 0xc3, 0xa3, 0xea, 0x60, 0x18, 0x93, 0x13, 0x7c, 0x98, 0xab, 0x74, + 0x02, 0x56, 0xd7, 0x45, 0xfe, 0x85, 0xb0, 0x2a, 0x88, 0x8d, 0x8f, 0xe0, 0x5d, 0x2e, 0x05, 0x08, + 0x6d, 0x1d, 0xb8, 0xc8, 0x3f, 0x0c, 0x7f, 0xd7, 0xde, 0x00, 0x5f, 0x3f, 0x03, 0xfd, 0x84, 0xf3, + 0xe6, 0x90, 0x10, 0xa6, 0x25, 0x14, 0xda, 0x7b, 0x8d, 0xed, 0xbf, 0x35, 0x8b, 0x5c, 0x8a, 0x02, + 0xc8, 0x63, 0xdc, 0x6f, 0x79, 0xb6, 0x90, 0x7b, 0xe0, 0x5f, 0x3a, 0x1d, 0x04, 0x2d, 0xd3, 0x41, + 0xeb, 0xee, 0x71, 0xd6, 0xa8, 0xbc, 0x47, 0xf8, 0xea, 0x19, 0xe8, 0x06, 0xa0, 0x1e, 0x4c, 0x6e, + 0x62, 0x3c, 0x2d, 0x41, 0xcd, 0xa3, 0x98, 0x69, 0x66, 0x5c, 0xf6, 0xc2, 0x9e, 0x39, 0x79, 0xc6, + 0x34, 0xf3, 0x46, 0xf8, 0xda, 0xf6, 0xbd, 0xfd, 0x9a, 0xd0, 0xb9, 0x34, 0x9d, 0x7e, 0xe9, 0xe2, + 0x2b, 0xcd, 0xf6, 0x0b, 0x50, 0xb3, 0x4d, 0x88, 0x0b, 0x84, 0xc9, 0x6e, 0x18, 0xc4, 0xdf, 0x62, + 0xde, 0x1b, 0xa6, 0x7d, 0xf7, 0x3f, 0x90, 0x95, 0x0b, 0xcf, 0xff, 0xf8, 0xed, 0xe7, 0xa2, 0xeb, + 0x11, 0x97, 0x56, 0x4f, 0x6c, 0xf3, 0x32, 0x12, 0xd0, 0x11, 0xe3, 0x3c, 0x6a, 0xd9, 0x23, 0x9f, + 0x10, 0xbe, 0xdc, 0x8e, 0x82, 0xdc, 0xde, 0x9d, 0xb3, 0x9b, 0xb0, 0x7d, 0xe7, 0x1f, 0xa8, 0x5a, + 0xc9, 0x03, 0xa3, 0xe4, 0x1e, 0xf1, 0xb7, 0x94, 0x34, 0x55, 0xd0, 0xf7, 0x7f, 0xfe, 0xd5, 0x87, + 0xa7, 0xc3, 0xaf, 0x2b, 0x07, 0x2d, 0x57, 0x0e, 0xfa, 0xb1, 0x72, 0xd0, 0xe7, 0xb5, 0xd3, 0x59, + 0xae, 0x9d, 0xce, 0xf7, 0xb5, 0xd3, 0x19, 0xd1, 0x24, 0xd5, 0x6f, 0xca, 0x71, 0x30, 0x91, 0x19, + 0xd5, 0xc0, 0xb9, 0x54, 0xf7, 0x53, 0x49, 0xdb, 0x4b, 0x54, 0x80, 0x9a, 0x81, 0xa2, 0x7a, 0x9e, + 0x43, 0x31, 0xbe, 0x68, 0x36, 0xe0, 0xe1, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd8, 0xe9, 0xa4, + 0xbf, 0x69, 0x03, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -210,8 +318,10 @@ const _ = grpc.SupportPackageIsVersion4 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type MedianValuesServiceClient interface { - // GetMedianValues returns the median values for all markets - GetMedianValues(ctx context.Context, in *GetMedianValuesRequest, opts ...grpc.CallOption) (*GetMedianValuesResponse, error) + // GetAllMedianValues returns the median values for all markets + GetAllMedianValues(ctx context.Context, in *GetAllMedianValuesRequest, opts ...grpc.CallOption) (*GetAllMedianValuesResponse, error) + // GetMedianValue returns the median value for a market given a query data that a market is associated with + GetMedianValue(ctx context.Context, in *GetMedianValueRequest, opts ...grpc.CallOption) (*GetMedianValueResponse, error) } type medianValuesServiceClient struct { @@ -222,9 +332,18 @@ func NewMedianValuesServiceClient(cc grpc1.ClientConn) MedianValuesServiceClient return &medianValuesServiceClient{cc} } -func (c *medianValuesServiceClient) GetMedianValues(ctx context.Context, in *GetMedianValuesRequest, opts ...grpc.CallOption) (*GetMedianValuesResponse, error) { - out := new(GetMedianValuesResponse) - err := c.cc.Invoke(ctx, "/layer.daemons.MedianValuesService/GetMedianValues", in, out, opts...) +func (c *medianValuesServiceClient) GetAllMedianValues(ctx context.Context, in *GetAllMedianValuesRequest, opts ...grpc.CallOption) (*GetAllMedianValuesResponse, error) { + out := new(GetAllMedianValuesResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.MedianValuesService/GetAllMedianValues", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *medianValuesServiceClient) GetMedianValue(ctx context.Context, in *GetMedianValueRequest, opts ...grpc.CallOption) (*GetMedianValueResponse, error) { + out := new(GetMedianValueResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.MedianValuesService/GetMedianValue", in, out, opts...) if err != nil { return nil, err } @@ -233,36 +352,59 @@ func (c *medianValuesServiceClient) GetMedianValues(ctx context.Context, in *Get // MedianValuesServiceServer is the server API for MedianValuesService service. type MedianValuesServiceServer interface { - // GetMedianValues returns the median values for all markets - GetMedianValues(context.Context, *GetMedianValuesRequest) (*GetMedianValuesResponse, error) + // GetAllMedianValues returns the median values for all markets + GetAllMedianValues(context.Context, *GetAllMedianValuesRequest) (*GetAllMedianValuesResponse, error) + // GetMedianValue returns the median value for a market given a query data that a market is associated with + GetMedianValue(context.Context, *GetMedianValueRequest) (*GetMedianValueResponse, error) } // UnimplementedMedianValuesServiceServer can be embedded to have forward compatible implementations. type UnimplementedMedianValuesServiceServer struct { } -func (*UnimplementedMedianValuesServiceServer) GetMedianValues(ctx context.Context, req *GetMedianValuesRequest) (*GetMedianValuesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetMedianValues not implemented") +func (*UnimplementedMedianValuesServiceServer) GetAllMedianValues(ctx context.Context, req *GetAllMedianValuesRequest) (*GetAllMedianValuesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAllMedianValues not implemented") +} +func (*UnimplementedMedianValuesServiceServer) GetMedianValue(ctx context.Context, req *GetMedianValueRequest) (*GetMedianValueResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMedianValue not implemented") } func RegisterMedianValuesServiceServer(s grpc1.Server, srv MedianValuesServiceServer) { s.RegisterService(&_MedianValuesService_serviceDesc, srv) } -func _MedianValuesService_GetMedianValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetMedianValuesRequest) +func _MedianValuesService_GetAllMedianValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAllMedianValuesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MedianValuesServiceServer).GetAllMedianValues(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.daemons.MedianValuesService/GetAllMedianValues", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MedianValuesServiceServer).GetAllMedianValues(ctx, req.(*GetAllMedianValuesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MedianValuesService_GetMedianValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMedianValueRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(MedianValuesServiceServer).GetMedianValues(ctx, in) + return srv.(MedianValuesServiceServer).GetMedianValue(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/layer.daemons.MedianValuesService/GetMedianValues", + FullMethod: "/layer.daemons.MedianValuesService/GetMedianValue", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(MedianValuesServiceServer).GetMedianValues(ctx, req.(*GetMedianValuesRequest)) + return srv.(MedianValuesServiceServer).GetMedianValue(ctx, req.(*GetMedianValueRequest)) } return interceptor(ctx, in, info, handler) } @@ -272,8 +414,12 @@ var _MedianValuesService_serviceDesc = grpc.ServiceDesc{ HandlerType: (*MedianValuesServiceServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "GetMedianValues", - Handler: _MedianValuesService_GetMedianValues_Handler, + MethodName: "GetAllMedianValues", + Handler: _MedianValuesService_GetAllMedianValues_Handler, + }, + { + MethodName: "GetMedianValue", + Handler: _MedianValuesService_GetMedianValue_Handler, }, }, Streams: []grpc.StreamDesc{}, @@ -300,6 +446,11 @@ func (m *MedianValues) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Exponent != 0 { + i = encodeVarintMedianValues(dAtA, i, uint64(m.Exponent)) + i-- + dAtA[i] = 0x18 + } if m.Price != 0 { i = encodeVarintMedianValues(dAtA, i, uint64(m.Price)) i-- @@ -313,7 +464,7 @@ func (m *MedianValues) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *GetMedianValuesRequest) Marshal() (dAtA []byte, err error) { +func (m *GetAllMedianValuesRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -323,12 +474,12 @@ func (m *GetMedianValuesRequest) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *GetMedianValuesRequest) MarshalTo(dAtA []byte) (int, error) { +func (m *GetAllMedianValuesRequest) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *GetMedianValuesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *GetAllMedianValuesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int @@ -336,7 +487,7 @@ func (m *GetMedianValuesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) return len(dAtA) - i, nil } -func (m *GetMedianValuesResponse) Marshal() (dAtA []byte, err error) { +func (m *GetAllMedianValuesResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -346,12 +497,12 @@ func (m *GetMedianValuesResponse) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *GetMedianValuesResponse) MarshalTo(dAtA []byte) (int, error) { +func (m *GetAllMedianValuesResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *GetMedianValuesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *GetAllMedianValuesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int @@ -373,6 +524,71 @@ func (m *GetMedianValuesResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) return len(dAtA) - i, nil } +func (m *GetMedianValueRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetMedianValueRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GetMedianValueRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.QueryData) > 0 { + i -= len(m.QueryData) + copy(dAtA[i:], m.QueryData) + i = encodeVarintMedianValues(dAtA, i, uint64(len(m.QueryData))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *GetMedianValueResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetMedianValueResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GetMedianValueResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.MedianValues != nil { + { + size, err := m.MedianValues.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMedianValues(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintMedianValues(dAtA []byte, offset int, v uint64) int { offset -= sovMedianValues(v) base := offset @@ -396,10 +612,13 @@ func (m *MedianValues) Size() (n int) { if m.Price != 0 { n += 1 + sovMedianValues(uint64(m.Price)) } + if m.Exponent != 0 { + n += 1 + sovMedianValues(uint64(m.Exponent)) + } return n } -func (m *GetMedianValuesRequest) Size() (n int) { +func (m *GetAllMedianValuesRequest) Size() (n int) { if m == nil { return 0 } @@ -408,7 +627,7 @@ func (m *GetMedianValuesRequest) Size() (n int) { return n } -func (m *GetMedianValuesResponse) Size() (n int) { +func (m *GetAllMedianValuesResponse) Size() (n int) { if m == nil { return 0 } @@ -423,6 +642,32 @@ func (m *GetMedianValuesResponse) Size() (n int) { return n } +func (m *GetMedianValueRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.QueryData) + if l > 0 { + n += 1 + l + sovMedianValues(uint64(l)) + } + return n +} + +func (m *GetMedianValueResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.MedianValues != nil { + l = m.MedianValues.Size() + n += 1 + l + sovMedianValues(uint64(l)) + } + return n +} + func sovMedianValues(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -496,6 +741,25 @@ func (m *MedianValues) Unmarshal(dAtA []byte) error { break } } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Exponent", wireType) + } + m.Exponent = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Exponent |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipMedianValues(dAtA[iNdEx:]) @@ -517,7 +781,7 @@ func (m *MedianValues) Unmarshal(dAtA []byte) error { } return nil } -func (m *GetMedianValuesRequest) Unmarshal(dAtA []byte) error { +func (m *GetAllMedianValuesRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -540,10 +804,10 @@ func (m *GetMedianValuesRequest) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: GetMedianValuesRequest: wiretype end group for non-group") + return fmt.Errorf("proto: GetAllMedianValuesRequest: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: GetMedianValuesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: GetAllMedianValuesRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { default: @@ -567,7 +831,7 @@ func (m *GetMedianValuesRequest) Unmarshal(dAtA []byte) error { } return nil } -func (m *GetMedianValuesResponse) Unmarshal(dAtA []byte) error { +func (m *GetAllMedianValuesResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -590,10 +854,10 @@ func (m *GetMedianValuesResponse) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: GetMedianValuesResponse: wiretype end group for non-group") + return fmt.Errorf("proto: GetAllMedianValuesResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: GetMedianValuesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: GetAllMedianValuesResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -651,6 +915,174 @@ func (m *GetMedianValuesResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *GetMedianValueRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetMedianValueRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetMedianValueRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field QueryData", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthMedianValues + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthMedianValues + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.QueryData = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMedianValues(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMedianValues + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetMedianValueResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetMedianValueResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetMedianValueResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MedianValues", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMedianValues + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMedianValues + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMedianValues + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MedianValues == nil { + m.MedianValues = &MedianValues{} + } + if err := m.MedianValues.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMedianValues(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMedianValues + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipMedianValues(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/daemons/server/types/median_values.pb.gw.go b/daemons/server/types/median_values.pb.gw.go index 6c1a03304..2e04636cb 100644 --- a/daemons/server/types/median_values.pb.gw.go +++ b/daemons/server/types/median_values.pb.gw.go @@ -33,20 +33,74 @@ var _ = utilities.NewDoubleArray var _ = descriptor.ForMessage var _ = metadata.Join -func request_MedianValuesService_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, client MedianValuesServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetMedianValuesRequest +func request_MedianValuesService_GetAllMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, client MedianValuesServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAllMedianValuesRequest var metadata runtime.ServerMetadata - msg, err := client.GetMedianValues(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetAllMedianValues(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_MedianValuesService_GetMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, server MedianValuesServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetMedianValuesRequest +func local_request_MedianValuesService_GetAllMedianValues_0(ctx context.Context, marshaler runtime.Marshaler, server MedianValuesServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAllMedianValuesRequest var metadata runtime.ServerMetadata - msg, err := server.GetMedianValues(ctx, &protoReq) + msg, err := server.GetAllMedianValues(ctx, &protoReq) + return msg, metadata, err + +} + +func request_MedianValuesService_GetMedianValue_0(ctx context.Context, marshaler runtime.Marshaler, client MedianValuesServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetMedianValueRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["query_data"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query_data") + } + + protoReq.QueryData, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query_data", err) + } + + msg, err := client.GetMedianValue(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_MedianValuesService_GetMedianValue_0(ctx context.Context, marshaler runtime.Marshaler, server MedianValuesServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetMedianValueRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["query_data"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "query_data") + } + + protoReq.QueryData, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "query_data", err) + } + + msg, err := server.GetMedianValue(ctx, &protoReq) return msg, metadata, err } @@ -57,7 +111,7 @@ func local_request_MedianValuesService_GetMedianValues_0(ctx context.Context, ma // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterMedianValuesServiceHandlerFromEndpoint instead. func RegisterMedianValuesServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server MedianValuesServiceServer) error { - mux.Handle("GET", pattern_MedianValuesService_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_MedianValuesService_GetAllMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -68,7 +122,7 @@ func RegisterMedianValuesServiceHandlerServer(ctx context.Context, mux *runtime. runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_MedianValuesService_GetMedianValues_0(rctx, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_MedianValuesService_GetAllMedianValues_0(rctx, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { @@ -76,7 +130,30 @@ func RegisterMedianValuesServiceHandlerServer(ctx context.Context, mux *runtime. return } - forward_MedianValuesService_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_MedianValuesService_GetAllMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_MedianValuesService_GetMedianValue_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MedianValuesService_GetMedianValue_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_MedianValuesService_GetMedianValue_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -121,7 +198,27 @@ func RegisterMedianValuesServiceHandler(ctx context.Context, mux *runtime.ServeM // "MedianValuesServiceClient" to call the correct interceptors. func RegisterMedianValuesServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client MedianValuesServiceClient) error { - mux.Handle("GET", pattern_MedianValuesService_GetMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_MedianValuesService_GetAllMedianValues_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MedianValuesService_GetAllMedianValues_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_MedianValuesService_GetAllMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_MedianValuesService_GetMedianValue_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) @@ -130,14 +227,14 @@ func RegisterMedianValuesServiceHandlerClient(ctx context.Context, mux *runtime. runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_MedianValuesService_GetMedianValues_0(rctx, inboundMarshaler, client, req, pathParams) + resp, md, err := request_MedianValuesService_GetMedianValue_0(rctx, inboundMarshaler, client, req, pathParams) ctx = runtime.NewServerMetadataContext(ctx, md) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - forward_MedianValuesService_GetMedianValues_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_MedianValuesService_GetMedianValue_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -145,9 +242,13 @@ func RegisterMedianValuesServiceHandlerClient(ctx context.Context, mux *runtime. } var ( - pattern_MedianValuesService_GetMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"layer", "api", "get_median_values"}, "", runtime.AssumeColonVerbOpt(true))) + pattern_MedianValuesService_GetAllMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"layer", "api", "get_all_median_values"}, "", runtime.AssumeColonVerbOpt(true))) + + pattern_MedianValuesService_GetMedianValue_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"layer", "api", "get_median_value", "query_data"}, "", runtime.AssumeColonVerbOpt(true))) ) var ( - forward_MedianValuesService_GetMedianValues_0 = runtime.ForwardResponseMessage + forward_MedianValuesService_GetAllMedianValues_0 = runtime.ForwardResponseMessage + + forward_MedianValuesService_GetMedianValue_0 = runtime.ForwardResponseMessage ) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 183eeb8b5..5c82275ae 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46437,700 +46437,6 @@ paths: } tags: - Query - /tellor-io/layer/dispute/params: - get: - summary: Parameters queries the parameters of the module. - operationId: LayerDisputeParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query - /layer/oracle/get_reportsby_qid/{queryId}: - get: - summary: Queries a list of GetReportsbyQid items. - operationId: LayerOracleGetReportsbyQid - responses: - '200': - description: A successful response. - schema: - type: object - properties: - reports: - type: object - properties: - microReports: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: - type: string - value: - type: string - blockNumber: - type: string - format: int64 - timestamp: - type: string - format: date-time - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: queryId - in: path - required: true - type: string - tags: - - Query - /layer/oracle/get_reportsby_reporter/{reporter}: - get: - operationId: LayerOracleGetReportsbyReporter - responses: - '200': - description: A successful response. - schema: - type: object - properties: - microReports: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: - type: string - value: - type: string - blockNumber: - type: string - format: int64 - timestamp: - type: string - format: date-time - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: reporter - in: path - required: true - type: string - tags: - - Query - /layer/oracle/get_reportsby_reporter_qid/{reporter}/{queryId}: - get: - operationId: LayerOracleGetReportsbyReporterQid - responses: - '200': - description: A successful response. - schema: - type: object - properties: - reports: - type: object - properties: - microReports: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: - type: string - value: - type: string - blockNumber: - type: string - format: int64 - timestamp: - type: string - format: date-time - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: reporter - in: path - required: true - type: string - - name: queryId - in: path - required: true - type: string - tags: - - Query - /layer/oracle/params: - get: - summary: Parameters queries the parameters of the module. - operationId: LayerOracleParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - properties: - cycle_list: - type: array - items: - type: string - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query - /tellor-io/layer/oracle/GetDataBefore/{queryId}/{timestamp}: - get: - summary: Queries a list of GetAggregatedReport items. - operationId: LayerOracleGetDataBefore - responses: - '200': - description: A successful response. - schema: - type: object - properties: - report: - type: object - properties: - queryId: - type: string - aggregateValue: - type: string - aggregateReporter: - type: string - reporterPower: - type: string - format: int64 - standardDeviation: - type: number - format: double - reporters: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - flagged: - type: boolean - nonce: - type: string - format: int64 - aggregateReportIndex: - type: string - format: int64 - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: queryId - in: path - required: true - type: string - - name: timestamp - in: path - required: true - type: string - format: int64 - tags: - - Query - /tellor-io/layer/oracle/get_aggregated_report/{queryId}: - get: - summary: Queries a list of GetAggregatedReport items. - operationId: LayerOracleGetAggregatedReport - responses: - '200': - description: A successful response. - schema: - type: object - properties: - report: - type: object - properties: - queryId: - type: string - aggregateValue: - type: string - aggregateReporter: - type: string - reporterPower: - type: string - format: int64 - standardDeviation: - type: number - format: double - reporters: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - flagged: - type: boolean - nonce: - type: string - format: int64 - aggregateReportIndex: - type: string - format: int64 - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: queryId - in: path - required: true - type: string - tags: - - Query - /tellor-io/layer/oracle/get_current_tip/{queryData}: - get: - summary: Queries a list of GetCurrentTip items. - operationId: LayerOracleGetCurrentTip - responses: - '200': - description: A successful response. - schema: - type: object - properties: - tips: - type: object - properties: - queryData: - type: string - amount: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - - NOTE: The amount field is an Int which implements the - custom method - - signatures required by gogoproto. - totalTips: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - - NOTE: The amount field is an Int which implements the - custom method - - signatures required by gogoproto. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: queryData - in: path - required: true - type: string - tags: - - Query - /tellor-io/layer/oracle/get_time_based_rewards: - get: - summary: Queries a list of GetTimeBasedRewards items. - operationId: LayerOracleGetTimeBasedRewards - responses: - '200': - description: A successful response. - schema: - type: object - properties: - reward: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - - NOTE: The amount field is an Int which implements the custom - method - - signatures required by gogoproto. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query - /tellor-io/layer/oracle/get_user_tip_total/{tipper}/{queryData}: - get: - summary: Queries a list of GetUserTipTotal items. - operationId: LayerOracleGetUserTipTotal - responses: - '200': - description: A successful response. - schema: - type: object - properties: - totalTips: - type: object - properties: - address: - type: string - total: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - - NOTE: The amount field is an Int which implements the - custom method - - signatures required by gogoproto. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: tipper - in: path - required: true - type: string - - name: queryData - in: path - required: true - type: string - tags: - - Query - /layer/registry/get_data_spec/{queryType}: - get: - summary: Queries a list of GetDataSpec items. - operationId: LayerRegistryGetDataSpec - responses: - '200': - description: A successful response. - schema: - type: object - properties: - spec: - type: object - properties: - documentHash: - type: string - valueType: - type: string - aggregationMethod: - type: string - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: queryType - in: path - required: true - type: string - tags: - - Query - /layer/registry/get_query_data/{queryId}: - get: - summary: Queries a list of GetQueryData items. - operationId: LayerRegistryGetQueryData - responses: - '200': - description: A successful response. - schema: - type: object - properties: - queryData: - type: string - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - parameters: - - name: queryId - in: path - required: true - type: string - tags: - - Query - /layer/registry/params: - get: - summary: Parameters queries the parameters of the module. - operationId: LayerRegistryParams - responses: - '200': - description: A successful response. - schema: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - description: >- - QueryParamsResponse is response type for the Query/Params RPC - method. - default: - description: An unexpected error response. - schema: - type: object - properties: - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - '@type': - type: string - additionalProperties: {} - tags: - - Query definitions: cosmos.auth.v1beta1.AddressBytesToStringResponse: type: object @@ -72812,287 +72118,27 @@ definitions: properties: next_key: type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if - PageRequest.count_total - - was set, its value is undefined otherwise - description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. - - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - height: - title: query block height - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - title: |- - QueryPacketAcknowledgemetsResponse is the request type for the - Query/QueryPacketAcknowledgements RPC method - ibc.core.channel.v1.QueryPacketCommitmentResponse: - type: object - properties: - commitment: - type: string - format: byte - title: packet associated with the request fields - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - title: >- - QueryPacketCommitmentResponse defines the client query response for a - packet - - which also includes a proof and the height from which the proof was - - retrieved - ibc.core.channel.v1.QueryPacketCommitmentsResponse: - type: object - properties: - commitments: - type: array - items: - type: object - properties: - port_id: - type: string - description: channel port identifier. - channel_id: - type: string - description: channel unique identifier. - sequence: - type: string - format: uint64 - description: packet sequence. - data: - type: string - format: byte - description: embedded data that represents packet state. - description: >- - PacketState defines the generic type necessary to retrieve and store - - packet commitments, acknowledgements, and receipts. - - Caller is responsible for knowing the context necessary to interpret - this - - state as a commitment, acknowledgement, or a receipt. - pagination: - title: pagination response - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if - PageRequest.count_total - - was set, its value is undefined otherwise - description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. - - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - height: - title: query block height - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - title: |- - QueryPacketCommitmentsResponse is the request type for the - Query/QueryPacketCommitments RPC method - ibc.core.channel.v1.QueryPacketReceiptResponse: - type: object - properties: - received: - type: boolean - title: success flag for if receipt exists - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - title: >- - QueryPacketReceiptResponse defines the client query response for a packet - - receipt which also includes a proof, and the height from which the proof - was - - retrieved - ibc.core.channel.v1.QueryUnreceivedAcksResponse: - type: object - properties: - sequences: - type: array - items: - type: string - format: uint64 - title: list of unreceived acknowledgement sequences - height: - title: query block height - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: type: string format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that + title: >- + total is total number of results available if + PageRequest.count_total - height continues to be monitonically increasing even as the - RevisionHeight + was set, its value is undefined otherwise + description: |- + PageResponse is to be embedded in gRPC response messages where the + corresponding request message has used PageRequest. - gets reset - title: |- - QueryUnreceivedAcksResponse is the response type for the - Query/UnreceivedAcks RPC method - ibc.core.channel.v1.QueryUnreceivedPacketsResponse: - type: object - properties: - sequences: - type: array - items: - type: string - format: uint64 - title: list of unreceived packet sequences + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } height: title: query block height type: object @@ -73122,214 +72168,21 @@ definitions: gets reset title: |- - QueryUnreceivedPacketsResponse is the response type for the - Query/UnreceivedPacketCommitments RPC method - ibc.core.channel.v1.ResponseResultType: - type: string - enum: - - RESPONSE_RESULT_TYPE_UNSPECIFIED - - RESPONSE_RESULT_TYPE_NOOP - - RESPONSE_RESULT_TYPE_SUCCESS - default: RESPONSE_RESULT_TYPE_UNSPECIFIED - description: |- - - RESPONSE_RESULT_TYPE_UNSPECIFIED: Default zero value enumeration - - RESPONSE_RESULT_TYPE_NOOP: The message did not call the IBC application callbacks (because, for example, the packet had already been relayed) - - RESPONSE_RESULT_TYPE_SUCCESS: The message was executed successfully - title: >- - ResponseResultType defines the possible outcomes of the execution of a - message - ibc.core.channel.v1.State: - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - - STATE_CLOSED - default: STATE_UNINITIALIZED_UNSPECIFIED - description: |- - State defines if a channel is in one of the following states: - CLOSED, INIT, TRYOPEN, OPEN or UNINITIALIZED. - - - STATE_UNINITIALIZED_UNSPECIFIED: Default State - - STATE_INIT: A channel has just started the opening handshake. - - STATE_TRYOPEN: A channel has acknowledged the handshake step on the counterparty chain. - - STATE_OPEN: A channel has completed the handshake. Open channels are - ready to send and receive packets. - - STATE_CLOSED: A channel has been closed and can no longer be used to send or receive - packets. - ibc.core.client.v1.IdentifiedClientState: + QueryPacketAcknowledgemetsResponse is the request type for the + Query/QueryPacketAcknowledgements RPC method + ibc.core.channel.v1.QueryPacketCommitmentResponse: type: object properties: - client_id: + commitment: type: string - title: client identifier - client_state: - title: client state - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a - canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary all types - that they - - expect it to use in the context of Any. However, for URLs which - use the - - scheme `http`, `https`, or no scheme, one can optionally set up a - type - - server that maps type URLs to message definitions as follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the - official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer message along - with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values in the - form - - of utility functions or additional generated methods of the Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - - - JSON - - ==== - - The JSON representation of an `Any` value uses the regular - - representation of the deserialized, embedded message, with an - - additional field `@type` which contains the type URL. Example: - - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } - - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } - - If the embedded message type is well-known and has a custom JSON - - representation, that representation will be embedded adding a field - - `value` which holds the custom JSON in addition to the `@type` - - field. Example (for message [google.protobuf.Duration][]): - - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: |- - IdentifiedClientState defines a client state with an additional client - identifier field. - ibc.core.client.v1.ConsensusStateWithHeight: - type: object - properties: - height: - title: consensus state height + format: byte + title: packet associated with the request fields + proof: + type: string + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved type: object properties: revision_number: @@ -73356,229 +72209,269 @@ definitions: RevisionHeight gets reset - consensus_state: - title: consensus state - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a - canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary all types - that they - - expect it to use in the context of Any. However, for URLs which - use the - - scheme `http`, `https`, or no scheme, one can optionally set up a - type - - server that maps type URLs to message definitions as follows: - - - * If no scheme is provided, `https` is assumed. + title: >- + QueryPacketCommitmentResponse defines the client query response for a + packet - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) + which also includes a proof and the height from which the proof was - Note: this functionality is not currently available in the - official + retrieved + ibc.core.channel.v1.QueryPacketCommitmentsResponse: + type: object + properties: + commitments: + type: array + items: + type: object + properties: + port_id: + type: string + description: channel port identifier. + channel_id: + type: string + description: channel unique identifier. + sequence: + type: string + format: uint64 + description: packet sequence. + data: + type: string + format: byte + description: embedded data that represents packet state. + description: >- + PacketState defines the generic type necessary to retrieve and store - protobuf release, and it is not used for type URLs beginning with + packet commitments, acknowledgements, and receipts. - type.googleapis.com. + Caller is responsible for knowing the context necessary to interpret + this + state as a commitment, acknowledgement, or a receipt. + pagination: + title: pagination response + type: object + properties: + next_key: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: + type: string + format: uint64 + title: >- + total is total number of results available if + PageRequest.count_total - Schemes other than `http`, `https` (or the empty scheme) might be + was set, its value is undefined otherwise + description: |- + PageResponse is to be embedded in gRPC response messages where the + corresponding request message has used PageRequest. - used with implementation specific semantics. - additionalProperties: {} + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } + height: + title: query block height + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision description: >- - `Any` contains an arbitrary serialized protocol buffer message along - with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values in the - form - - of utility functions or additional generated methods of the Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - - - JSON + Normally the RevisionHeight is incremented at each height while + keeping - ==== + RevisionNumber the same. However some consensus algorithms may choose + to - The JSON representation of an `Any` value uses the regular + reset the height in certain conditions e.g. hard forks, state-machine - representation of the deserialized, embedded message, with an + breaking changes In these cases, the RevisionNumber is incremented so + that - additional field `@type` which contains the type URL. Example: + height continues to be monitonically increasing even as the + RevisionHeight - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + gets reset + title: |- + QueryPacketCommitmentsResponse is the request type for the + Query/QueryPacketCommitments RPC method + ibc.core.channel.v1.QueryPacketReceiptResponse: + type: object + properties: + received: + type: boolean + title: success flag for if receipt exists + proof: + type: string + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + RevisionNumber the same. However some consensus algorithms may choose + to - If the embedded message type is well-known and has a custom JSON + reset the height in certain conditions e.g. hard forks, state-machine - representation, that representation will be embedded adding a field + breaking changes In these cases, the RevisionNumber is incremented so + that - `value` which holds the custom JSON in addition to the `@type` + height continues to be monitonically increasing even as the + RevisionHeight - field. Example (for message [google.protobuf.Duration][]): + gets reset + title: >- + QueryPacketReceiptResponse defines the client query response for a packet - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - ConsensusStateWithHeight defines a consensus state with an additional - height + receipt which also includes a proof, and the height from which the proof + was - field. - ibc.core.client.v1.MsgCreateClientResponse: - type: object - description: MsgCreateClientResponse defines the Msg/CreateClient response type. - ibc.core.client.v1.MsgSubmitMisbehaviourResponse: - type: object - description: |- - MsgSubmitMisbehaviourResponse defines the Msg/SubmitMisbehaviour response - type. - ibc.core.client.v1.MsgUpdateClientResponse: - type: object - description: MsgUpdateClientResponse defines the Msg/UpdateClient response type. - ibc.core.client.v1.MsgUpgradeClientResponse: - type: object - description: MsgUpgradeClientResponse defines the Msg/UpgradeClient response type. - ibc.core.client.v1.Params: + retrieved + ibc.core.channel.v1.QueryUnreceivedAcksResponse: type: object properties: - allowed_clients: + sequences: type: array items: type: string + format: uint64 + title: list of unreceived acknowledgement sequences + height: + title: query block height + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision description: >- - allowed_clients defines the list of allowed client state types which - can be created + Normally the RevisionHeight is incremented at each height while + keeping - and interacted with. If a client type is removed from the allowed - clients list, usage + RevisionNumber the same. However some consensus algorithms may choose + to - of this client will be disabled until it is added again to the list. - description: Params defines the set of IBC light client parameters. - ibc.core.client.v1.QueryClientParamsResponse: + reset the height in certain conditions e.g. hard forks, state-machine + + breaking changes In these cases, the RevisionNumber is incremented so + that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + title: |- + QueryUnreceivedAcksResponse is the response type for the + Query/UnreceivedAcks RPC method + ibc.core.channel.v1.QueryUnreceivedPacketsResponse: type: object properties: - params: - description: params defines the parameters of the module. + sequences: + type: array + items: + type: string + format: uint64 + title: list of unreceived packet sequences + height: + title: query block height type: object properties: - allowed_clients: - type: array - items: - type: string - description: >- - allowed_clients defines the list of allowed client state types - which can be created + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping - and interacted with. If a client type is removed from the allowed - clients list, usage + RevisionNumber the same. However some consensus algorithms may choose + to - of this client will be disabled until it is added again to the - list. - description: >- - QueryClientParamsResponse is the response type for the Query/ClientParams - RPC + reset the height in certain conditions e.g. hard forks, state-machine - method. - ibc.core.client.v1.QueryClientStateResponse: + breaking changes In these cases, the RevisionNumber is incremented so + that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + title: |- + QueryUnreceivedPacketsResponse is the response type for the + Query/UnreceivedPacketCommitments RPC method + ibc.core.channel.v1.ResponseResultType: + type: string + enum: + - RESPONSE_RESULT_TYPE_UNSPECIFIED + - RESPONSE_RESULT_TYPE_NOOP + - RESPONSE_RESULT_TYPE_SUCCESS + default: RESPONSE_RESULT_TYPE_UNSPECIFIED + description: |- + - RESPONSE_RESULT_TYPE_UNSPECIFIED: Default zero value enumeration + - RESPONSE_RESULT_TYPE_NOOP: The message did not call the IBC application callbacks (because, for example, the packet had already been relayed) + - RESPONSE_RESULT_TYPE_SUCCESS: The message was executed successfully + title: >- + ResponseResultType defines the possible outcomes of the execution of a + message + ibc.core.channel.v1.State: + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + - STATE_CLOSED + default: STATE_UNINITIALIZED_UNSPECIFIED + description: |- + State defines if a channel is in one of the following states: + CLOSED, INIT, TRYOPEN, OPEN or UNINITIALIZED. + + - STATE_UNINITIALIZED_UNSPECIFIED: Default State + - STATE_INIT: A channel has just started the opening handshake. + - STATE_TRYOPEN: A channel has acknowledged the handshake step on the counterparty chain. + - STATE_OPEN: A channel has completed the handshake. Open channels are + ready to send and receive packets. + - STATE_CLOSED: A channel has been closed and can no longer be used to send or receive + packets. + ibc.core.client.v1.IdentifiedClientState: type: object properties: + client_id: + type: string + title: client identifier client_state: - title: client state associated with the request identifier + title: client state type: object properties: '@type': @@ -73735,12 +72628,14 @@ definitions: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved + description: |- + IdentifiedClientState defines a client state with an additional client + identifier field. + ibc.core.client.v1.ConsensusStateWithHeight: + type: object + properties: + height: + title: consensus state height type: object properties: revision_number: @@ -73767,318 +72662,229 @@ definitions: RevisionHeight gets reset - description: >- - QueryClientStateResponse is the response type for the Query/ClientState - RPC - - method. Besides the client state, it includes a proof and the height from - - which the proof was retrieved. - ibc.core.client.v1.QueryClientStatesResponse: - type: object - properties: - client_states: - type: array - items: - type: object - properties: - client_id: - type: string - title: client identifier - client_state: - title: client state - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in + consensus_state: + title: consensus state + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of the + serialized - `path/google.protobuf.Duration`). The name should be in a - canonical form + protocol buffer message. This string must contain at least - (e.g., leading "." is not accepted). + one "/" character. The last segment of the URL's path must + represent + the fully qualified name of the type (as in - In practice, teams usually precompile into the binary all - types that they + `path/google.protobuf.Duration`). The name should be in a + canonical form - expect it to use in the context of Any. However, for URLs - which use the + (e.g., leading "." is not accepted). - scheme `http`, `https`, or no scheme, one can optionally set - up a type - server that maps type URLs to message definitions as - follows: + In practice, teams usually precompile into the binary all types + that they + expect it to use in the context of Any. However, for URLs which + use the - * If no scheme is provided, `https` is assumed. + scheme `http`, `https`, or no scheme, one can optionally set up a + type - * An HTTP GET on the URL must yield a - [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on - the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) + server that maps type URLs to message definitions as follows: - Note: this functionality is not currently available in the - official - protobuf release, and it is not used for type URLs beginning - with + * If no scheme is provided, `https` is assumed. - type.googleapis.com. + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + Note: this functionality is not currently available in the + official - Schemes other than `http`, `https` (or the empty scheme) - might be + protobuf release, and it is not used for type URLs beginning with - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer message - along with a + type.googleapis.com. - URL that describes the type of the serialized message. + Schemes other than `http`, `https` (or the empty scheme) might be - Protobuf library provides support to pack/unpack Any values in - the form + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message along + with a - of utility functions or additional generated methods of the Any - type. + URL that describes the type of the serialized message. - Example 1: Pack and unpack a message in C++. + Protobuf library provides support to pack/unpack Any values in the + form - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } + of utility functions or additional generated methods of the Any type. - Example 2: Pack and unpack a message in Java. - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } + Example 1: Pack and unpack a message in C++. - Example 3: Pack and unpack a message in Python. + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... + Example 2: Pack and unpack a message in Java. - Example 4: Pack and unpack a message in Go + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } + Example 3: Pack and unpack a message in Python. - The pack methods provided by protobuf library will by default - use + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - 'type.googleapis.com/full.type.name' as the type URL and the - unpack + Example 4: Pack and unpack a message in Go - methods only use the fully qualified type name after the last - '/' + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } - in the type URL, for example "foo.bar.com/x/y.z" will yield type + The pack methods provided by protobuf library will by default use - name "y.z". + 'type.googleapis.com/full.type.name' as the type URL and the unpack + methods only use the fully qualified type name after the last '/' + in the type URL, for example "foo.bar.com/x/y.z" will yield type - JSON + name "y.z". - ==== - The JSON representation of an `Any` value uses the regular - representation of the deserialized, embedded message, with an + JSON - additional field `@type` which contains the type URL. Example: + ==== - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + The JSON representation of an `Any` value uses the regular - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + representation of the deserialized, embedded message, with an - If the embedded message type is well-known and has a custom JSON + additional field `@type` which contains the type URL. Example: - representation, that representation will be embedded adding a - field + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } - `value` which holds the custom JSON in addition to the `@type` + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } - field. Example (for message [google.protobuf.Duration][]): + If the embedded message type is well-known and has a custom JSON - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: >- - IdentifiedClientState defines a client state with an additional - client + representation, that representation will be embedded adding a field - identifier field. - description: list of stored ClientStates of the chain. - pagination: - title: pagination response - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if - PageRequest.count_total + `value` which holds the custom JSON in addition to the `@type` - was set, its value is undefined otherwise - description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. + field. Example (for message [google.protobuf.Duration][]): - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } description: >- - QueryClientStatesResponse is the response type for the Query/ClientStates - RPC + ConsensusStateWithHeight defines a consensus state with an additional + height - method. - ibc.core.client.v1.QueryClientStatusResponse: + field. + ibc.core.client.v1.MsgCreateClientResponse: type: object - properties: - status: - type: string - description: >- - QueryClientStatusResponse is the response type for the Query/ClientStatus - RPC - - method. It returns the current status of the IBC client. - ibc.core.client.v1.QueryConsensusStateHeightsResponse: + description: MsgCreateClientResponse defines the Msg/CreateClient response type. + ibc.core.client.v1.MsgSubmitMisbehaviourResponse: + type: object + description: |- + MsgSubmitMisbehaviourResponse defines the Msg/SubmitMisbehaviour response + type. + ibc.core.client.v1.MsgUpdateClientResponse: + type: object + description: MsgUpdateClientResponse defines the Msg/UpdateClient response type. + ibc.core.client.v1.MsgUpgradeClientResponse: + type: object + description: MsgUpgradeClientResponse defines the Msg/UpgradeClient response type. + ibc.core.client.v1.Params: type: object properties: - consensus_state_heights: + allowed_clients: type: array items: - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may - choose to - - reset the height in certain conditions e.g. hard forks, - state-machine - - breaking changes In these cases, the RevisionNumber is incremented - so that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - title: >- - Height is a monotonically increasing data type + type: string + description: >- + allowed_clients defines the list of allowed client state types which + can be created - that can be compared against another Height for the purposes of - updating and + and interacted with. If a client type is removed from the allowed + clients list, usage - freezing clients - title: consensus state heights - pagination: - title: pagination response + of this client will be disabled until it is added again to the list. + description: Params defines the set of IBC light client parameters. + ibc.core.client.v1.QueryClientParamsResponse: + type: object + properties: + params: + description: params defines the parameters of the module. type: object properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if - PageRequest.count_total + allowed_clients: + type: array + items: + type: string + description: >- + allowed_clients defines the list of allowed client state types + which can be created - was set, its value is undefined otherwise - description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. + and interacted with. If a client type is removed from the allowed + clients list, usage - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - title: |- - QueryConsensusStateHeightsResponse is the response type for the - Query/ConsensusStateHeights RPC method - ibc.core.client.v1.QueryConsensusStateResponse: + of this client will be disabled until it is added again to the + list. + description: >- + QueryClientParamsResponse is the response type for the Query/ClientParams + RPC + + method. + ibc.core.client.v1.QueryClientStateResponse: type: object properties: - consensus_state: - title: >- - consensus state associated with the client identifier at the given - height + client_state: + title: client state associated with the request identifier type: object properties: '@type': @@ -74267,50 +73073,26 @@ definitions: RevisionHeight gets reset - title: >- - QueryConsensusStateResponse is the response type for the - Query/ConsensusState + description: >- + QueryClientStateResponse is the response type for the Query/ClientState + RPC - RPC method - ibc.core.client.v1.QueryConsensusStatesResponse: + method. Besides the client state, it includes a proof and the height from + + which the proof was retrieved. + ibc.core.client.v1.QueryClientStatesResponse: type: object properties: - consensus_states: + client_states: type: array items: type: object properties: - height: - title: consensus state height - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may - choose to - - reset the height in certain conditions e.g. hard forks, - state-machine - - breaking changes In these cases, the RevisionNumber is - incremented so that - - height continues to be monitonically increasing even as the - RevisionHeight - - gets reset - consensus_state: - title: consensus state + client_id: + type: string + title: client identifier + client_state: + title: client state type: object properties: '@type': @@ -74478,11 +73260,11 @@ definitions: "value": "1.212s" } description: >- - ConsensusStateWithHeight defines a consensus state with an - additional height + IdentifiedClientState defines a client state with an additional + client - field. - title: consensus states associated with the identifier + identifier field. + description: list of stored ClientStates of the chain. pagination: title: pagination response type: object @@ -74510,178 +73292,99 @@ definitions: repeated Bar results = 1; PageResponse page = 2; } - title: |- - QueryConsensusStatesResponse is the response type for the - Query/ConsensusStates RPC method - ibc.core.client.v1.QueryUpgradedClientStateResponse: - type: object - properties: - upgraded_client_state: - title: client state associated with the request identifier - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a - canonical form - - (e.g., leading "." is not accepted). - - - In practice, teams usually precompile into the binary all types - that they - - expect it to use in the context of Any. However, for URLs which - use the - - scheme `http`, `https`, or no scheme, one can optionally set up a - type - - server that maps type URLs to message definitions as follows: - - - * If no scheme is provided, `https` is assumed. - - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) - - Note: this functionality is not currently available in the - official - - protobuf release, and it is not used for type URLs beginning with - - type.googleapis.com. - - - Schemes other than `http`, `https` (or the empty scheme) might be - - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer message along - with a - - URL that describes the type of the serialized message. - - - Protobuf library provides support to pack/unpack Any values in the - form - - of utility functions or additional generated methods of the Any type. - - - Example 1: Pack and unpack a message in C++. - - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - - Example 2: Pack and unpack a message in Java. - - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } - - Example 3: Pack and unpack a message in Python. - - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... - - Example 4: Pack and unpack a message in Go - - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } - - The pack methods provided by protobuf library will by default use - - 'type.googleapis.com/full.type.name' as the type URL and the unpack - - methods only use the fully qualified type name after the last '/' - - in the type URL, for example "foo.bar.com/x/y.z" will yield type - - name "y.z". - - - - JSON + description: >- + QueryClientStatesResponse is the response type for the Query/ClientStates + RPC - ==== + method. + ibc.core.client.v1.QueryClientStatusResponse: + type: object + properties: + status: + type: string + description: >- + QueryClientStatusResponse is the response type for the Query/ClientStatus + RPC - The JSON representation of an `Any` value uses the regular + method. It returns the current status of the IBC client. + ibc.core.client.v1.QueryConsensusStateHeightsResponse: + type: object + properties: + consensus_state_heights: + type: array + items: + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping - representation of the deserialized, embedded message, with an + RevisionNumber the same. However some consensus algorithms may + choose to - additional field `@type` which contains the type URL. Example: + reset the height in certain conditions e.g. hard forks, + state-machine - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + breaking changes In these cases, the RevisionNumber is incremented + so that - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + height continues to be monitonically increasing even as the + RevisionHeight - If the embedded message type is well-known and has a custom JSON + gets reset + title: >- + Height is a monotonically increasing data type - representation, that representation will be embedded adding a field + that can be compared against another Height for the purposes of + updating and - `value` which holds the custom JSON in addition to the `@type` + freezing clients + title: consensus state heights + pagination: + title: pagination response + type: object + properties: + next_key: + type: string + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: + type: string + format: uint64 + title: >- + total is total number of results available if + PageRequest.count_total - field. Example (for message [google.protobuf.Duration][]): + was set, its value is undefined otherwise + description: |- + PageResponse is to be embedded in gRPC response messages where the + corresponding request message has used PageRequest. - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: |- - QueryUpgradedClientStateResponse is the response type for the - Query/UpgradedClientState RPC method. - ibc.core.client.v1.QueryUpgradedConsensusStateResponse: + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } + title: |- + QueryConsensusStateHeightsResponse is the response type for the + Query/ConsensusStateHeights RPC method + ibc.core.client.v1.QueryConsensusStateResponse: type: object properties: - upgraded_consensus_state: - title: Consensus state associated with the request identifier + consensus_state: + title: >- + consensus state associated with the client identifier at the given + height type: object properties: '@type': @@ -74838,513 +73541,453 @@ definitions: "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.212s" } - description: |- - QueryUpgradedConsensusStateResponse is the response type for the - Query/UpgradedConsensusState RPC method. - ibc.core.commitment.v1.MerklePrefix: - type: object - properties: - key_prefix: + proof: type: string format: byte - title: |- - MerklePrefix is merkle path prefixed to the key. - The constructed key from the Path and the key will be append(Path.KeyPath, - append(Path.KeyPrefix, key...)) - ibc.core.connection.v1.ConnectionEnd: + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping + + RevisionNumber the same. However some consensus algorithms may choose + to + + reset the height in certain conditions e.g. hard forks, state-machine + + breaking changes In these cases, the RevisionNumber is incremented so + that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + title: >- + QueryConsensusStateResponse is the response type for the + Query/ConsensusState + + RPC method + ibc.core.client.v1.QueryConsensusStatesResponse: type: object properties: - client_id: - type: string - description: client associated with this connection. - versions: + consensus_states: type: array items: type: object properties: - identifier: - type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: >- - Version defines the versioning scheme used to negotiate the IBC - verison in + height: + title: consensus state height + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping + + RevisionNumber the same. However some consensus algorithms may + choose to + + reset the height in certain conditions e.g. hard forks, + state-machine + + breaking changes In these cases, the RevisionNumber is + incremented so that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + consensus_state: + title: consensus state + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of the + serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must + represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a + canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary all + types that they + + expect it to use in the context of Any. However, for URLs + which use the + + scheme `http`, `https`, or no scheme, one can optionally set + up a type + + server that maps type URLs to message definitions as + follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a + [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on + the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the + official + + protobuf release, and it is not used for type URLs beginning + with + + type.googleapis.com. - the connection handshake. - description: >- - IBC version which can be utilised to determine encodings or protocols - for - channels or packets utilising this connection. - state: - description: current state of the connection end. - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - counterparty: - description: counterparty chain associated with this connection. - type: object - properties: - client_id: - type: string - description: >- - identifies the client on the counterparty chain associated with a - given + Schemes other than `http`, `https` (or the empty scheme) + might be - connection. - connection_id: - type: string - description: >- - identifies the connection end on the counterparty chain associated - with a + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message + along with a - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. + URL that describes the type of the serialized message. - The constructed key from the Path and the key will be - append(Path.KeyPath, - append(Path.KeyPrefix, key...)) - delay_period: - type: string - format: uint64 - description: >- - delay period that must pass before a consensus state can be used for + Protobuf library provides support to pack/unpack Any values in + the form - packet-verification NOTE: delay period logic is only implemented by - some + of utility functions or additional generated methods of the Any + type. - clients. - description: |- - ConnectionEnd defines a stateful object on a chain connected to another - separate one. - NOTE: there must only be 2 defined ConnectionEnds to establish - a connection between two chains. - ibc.core.connection.v1.Counterparty: - type: object - properties: - client_id: - type: string - description: >- - identifies the client on the counterparty chain associated with a - given - connection. - connection_id: - type: string - description: >- - identifies the connection end on the counterparty chain associated - with a + Example 1: Pack and unpack a message in C++. - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - The constructed key from the Path and the key will be - append(Path.KeyPath, + Example 2: Pack and unpack a message in Java. - append(Path.KeyPrefix, key...)) - description: >- - Counterparty defines the counterparty chain associated with a connection - end. - ibc.core.connection.v1.IdentifiedConnection: - type: object - properties: - id: - type: string - description: connection identifier. - client_id: - type: string - description: client associated with this connection. - versions: - type: array - items: - type: object - properties: - identifier: - type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: >- - Version defines the versioning scheme used to negotiate the IBC - verison in + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } - the connection handshake. - title: >- - IBC version which can be utilised to determine encodings or protocols - for + Example 3: Pack and unpack a message in Python. - channels or packets utilising this connection - state: - description: current state of the connection end. - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - counterparty: - description: counterparty chain associated with this connection. - type: object - properties: - client_id: - type: string - description: >- - identifies the client on the counterparty chain associated with a - given + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - connection. - connection_id: - type: string - description: >- - identifies the connection end on the counterparty chain associated - with a + Example 4: Pack and unpack a message in Go - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } - The constructed key from the Path and the key will be - append(Path.KeyPath, + The pack methods provided by protobuf library will by default + use - append(Path.KeyPrefix, key...)) - delay_period: - type: string - format: uint64 - description: delay period associated with this connection. - description: |- - IdentifiedConnection defines a connection with additional connection - identifier field. - ibc.core.connection.v1.MsgConnectionOpenAckResponse: - type: object - description: >- - MsgConnectionOpenAckResponse defines the Msg/ConnectionOpenAck response - type. - ibc.core.connection.v1.MsgConnectionOpenConfirmResponse: - type: object - description: |- - MsgConnectionOpenConfirmResponse defines the Msg/ConnectionOpenConfirm - response type. - ibc.core.connection.v1.MsgConnectionOpenInitResponse: - type: object - description: |- - MsgConnectionOpenInitResponse defines the Msg/ConnectionOpenInit response - type. - ibc.core.connection.v1.MsgConnectionOpenTryResponse: - type: object - description: >- - MsgConnectionOpenTryResponse defines the Msg/ConnectionOpenTry response - type. - ibc.core.connection.v1.Params: - type: object - properties: - max_expected_time_per_block: - type: string - format: uint64 - description: >- - maximum expected time per block (in nanoseconds), used to enforce - block delay. This parameter should reflect the + 'type.googleapis.com/full.type.name' as the type URL and the + unpack + + methods only use the fully qualified type name after the last + '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: - largest amount of time that the chain might reasonably take to produce - the next block under normal operating + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } - conditions. A safe choice is 3-5x the expected time per block. - description: Params defines the set of Connection parameters. - ibc.core.connection.v1.QueryClientConnectionsResponse: - type: object - properties: - connection_paths: - type: array - items: - type: string - description: slice of all the connection paths associated with a client. - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was generated + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a + field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: >- + ConsensusStateWithHeight defines a consensus state with an + additional height + + field. + title: consensus states associated with the identifier + pagination: + title: pagination response type: object properties: - revision_number: + next_key: type: string - format: uint64 - title: the revision that the client is currently on - revision_height: + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: type: string format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping - - RevisionNumber the same. However some consensus algorithms may choose - to - - reset the height in certain conditions e.g. hard forks, state-machine - - breaking changes In these cases, the RevisionNumber is incremented so - that + title: >- + total is total number of results available if + PageRequest.count_total - height continues to be monitonically increasing even as the - RevisionHeight + was set, its value is undefined otherwise + description: |- + PageResponse is to be embedded in gRPC response messages where the + corresponding request message has used PageRequest. - gets reset + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } title: |- - QueryClientConnectionsResponse is the response type for the - Query/ClientConnections RPC method - ibc.core.connection.v1.QueryConnectionClientStateResponse: + QueryConsensusStatesResponse is the response type for the + Query/ConsensusStates RPC method + ibc.core.client.v1.QueryUpgradedClientStateResponse: type: object properties: - identified_client_state: - title: client state associated with the channel + upgraded_client_state: + title: client state associated with the request identifier type: object properties: - client_id: + '@type': type: string - title: client identifier - client_state: - title: client state - type: object - properties: - '@type': - type: string - description: >- - A URL/resource name that uniquely identifies the type of the - serialized - - protocol buffer message. This string must contain at least - - one "/" character. The last segment of the URL's path must - represent - - the fully qualified name of the type (as in - - `path/google.protobuf.Duration`). The name should be in a - canonical form - - (e.g., leading "." is not accepted). + description: >- + A URL/resource name that uniquely identifies the type of the + serialized + protocol buffer message. This string must contain at least - In practice, teams usually precompile into the binary all - types that they + one "/" character. The last segment of the URL's path must + represent - expect it to use in the context of Any. However, for URLs - which use the + the fully qualified name of the type (as in - scheme `http`, `https`, or no scheme, one can optionally set - up a type + `path/google.protobuf.Duration`). The name should be in a + canonical form - server that maps type URLs to message definitions as follows: + (e.g., leading "." is not accepted). - * If no scheme is provided, `https` is assumed. + In practice, teams usually precompile into the binary all types + that they - * An HTTP GET on the URL must yield a [google.protobuf.Type][] - value in binary format, or produce an error. - * Applications are allowed to cache lookup results based on - the - URL, or have them precompiled into a binary to avoid any - lookup. Therefore, binary compatibility needs to be preserved - on changes to types. (Use versioned type names to manage - breaking changes.) + expect it to use in the context of Any. However, for URLs which + use the - Note: this functionality is not currently available in the - official + scheme `http`, `https`, or no scheme, one can optionally set up a + type - protobuf release, and it is not used for type URLs beginning - with + server that maps type URLs to message definitions as follows: - type.googleapis.com. + * If no scheme is provided, `https` is assumed. - Schemes other than `http`, `https` (or the empty scheme) might - be + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) - used with implementation specific semantics. - additionalProperties: {} - description: >- - `Any` contains an arbitrary serialized protocol buffer message - along with a + Note: this functionality is not currently available in the + official - URL that describes the type of the serialized message. + protobuf release, and it is not used for type URLs beginning with + type.googleapis.com. - Protobuf library provides support to pack/unpack Any values in the - form - of utility functions or additional generated methods of the Any - type. + Schemes other than `http`, `https` (or the empty scheme) might be + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message along + with a - Example 1: Pack and unpack a message in C++. + URL that describes the type of the serialized message. - Foo foo = ...; - Any any; - any.PackFrom(foo); - ... - if (any.UnpackTo(&foo)) { - ... - } - Example 2: Pack and unpack a message in Java. + Protobuf library provides support to pack/unpack Any values in the + form - Foo foo = ...; - Any any = Any.pack(foo); - ... - if (any.is(Foo.class)) { - foo = any.unpack(Foo.class); - } + of utility functions or additional generated methods of the Any type. - Example 3: Pack and unpack a message in Python. - foo = Foo(...) - any = Any() - any.Pack(foo) - ... - if any.Is(Foo.DESCRIPTOR): - any.Unpack(foo) - ... + Example 1: Pack and unpack a message in C++. - Example 4: Pack and unpack a message in Go + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - foo := &pb.Foo{...} - any, err := anypb.New(foo) - if err != nil { - ... - } - ... - foo := &pb.Foo{} - if err := any.UnmarshalTo(foo); err != nil { - ... - } + Example 2: Pack and unpack a message in Java. - The pack methods provided by protobuf library will by default use + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } - 'type.googleapis.com/full.type.name' as the type URL and the - unpack + Example 3: Pack and unpack a message in Python. - methods only use the fully qualified type name after the last '/' + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - in the type URL, for example "foo.bar.com/x/y.z" will yield type + Example 4: Pack and unpack a message in Go - name "y.z". + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + The pack methods provided by protobuf library will by default use + 'type.googleapis.com/full.type.name' as the type URL and the unpack - JSON + methods only use the fully qualified type name after the last '/' - ==== + in the type URL, for example "foo.bar.com/x/y.z" will yield type - The JSON representation of an `Any` value uses the regular + name "y.z". - representation of the deserialized, embedded message, with an - additional field `@type` which contains the type URL. Example: - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + JSON - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + ==== - If the embedded message type is well-known and has a custom JSON + The JSON representation of an `Any` value uses the regular - representation, that representation will be embedded adding a - field + representation of the deserialized, embedded message, with an - `value` which holds the custom JSON in addition to the `@type` + additional field `@type` which contains the type URL. Example: - field. Example (for message [google.protobuf.Duration][]): + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - description: |- - IdentifiedClientState defines a client state with an additional client - identifier field. - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } - RevisionNumber the same. However some consensus algorithms may choose - to + If the embedded message type is well-known and has a custom JSON - reset the height in certain conditions e.g. hard forks, state-machine + representation, that representation will be embedded adding a field - breaking changes In these cases, the RevisionNumber is incremented so - that + `value` which holds the custom JSON in addition to the `@type` - height continues to be monitonically increasing even as the - RevisionHeight + field. Example (for message [google.protobuf.Duration][]): - gets reset - title: |- - QueryConnectionClientStateResponse is the response type for the - Query/ConnectionClientState RPC method - ibc.core.connection.v1.QueryConnectionConsensusStateResponse: + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: |- + QueryUpgradedClientStateResponse is the response type for the + Query/UpgradedClientState RPC method. + ibc.core.client.v1.QueryUpgradedConsensusStateResponse: type: object properties: - consensus_state: - title: consensus state associated with the channel + upgraded_consensus_state: + title: Consensus state associated with the request identifier type: object properties: '@type': @@ -75475,41 +74118,293 @@ definitions: representation of the deserialized, embedded message, with an - additional field `@type` which contains the type URL. Example: + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: |- + QueryUpgradedConsensusStateResponse is the response type for the + Query/UpgradedConsensusState RPC method. + ibc.core.commitment.v1.MerklePrefix: + type: object + properties: + key_prefix: + type: string + format: byte + title: |- + MerklePrefix is merkle path prefixed to the key. + The constructed key from the Path and the key will be append(Path.KeyPath, + append(Path.KeyPrefix, key...)) + ibc.core.connection.v1.ConnectionEnd: + type: object + properties: + client_id: + type: string + description: client associated with this connection. + versions: + type: array + items: + type: object + properties: + identifier: + type: string + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: >- + Version defines the versioning scheme used to negotiate the IBC + verison in + + the connection handshake. + description: >- + IBC version which can be utilised to determine encodings or protocols + for + + channels or packets utilising this connection. + state: + description: current state of the connection end. + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + counterparty: + description: counterparty chain associated with this connection. + type: object + properties: + client_id: + type: string + description: >- + identifies the client on the counterparty chain associated with a + given + + connection. + connection_id: + type: string + description: >- + identifies the connection end on the counterparty chain associated + with a + + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. + + The constructed key from the Path and the key will be + append(Path.KeyPath, + + append(Path.KeyPrefix, key...)) + delay_period: + type: string + format: uint64 + description: >- + delay period that must pass before a consensus state can be used for + + packet-verification NOTE: delay period logic is only implemented by + some + + clients. + description: |- + ConnectionEnd defines a stateful object on a chain connected to another + separate one. + NOTE: there must only be 2 defined ConnectionEnds to establish + a connection between two chains. + ibc.core.connection.v1.Counterparty: + type: object + properties: + client_id: + type: string + description: >- + identifies the client on the counterparty chain associated with a + given + + connection. + connection_id: + type: string + description: >- + identifies the connection end on the counterparty chain associated + with a + + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. + + The constructed key from the Path and the key will be + append(Path.KeyPath, - package google.profile; - message Person { - string first_name = 1; - string last_name = 2; - } + append(Path.KeyPrefix, key...)) + description: >- + Counterparty defines the counterparty chain associated with a connection + end. + ibc.core.connection.v1.IdentifiedConnection: + type: object + properties: + id: + type: string + description: connection identifier. + client_id: + type: string + description: client associated with this connection. + versions: + type: array + items: + type: object + properties: + identifier: + type: string + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: >- + Version defines the versioning scheme used to negotiate the IBC + verison in - { - "@type": "type.googleapis.com/google.profile.Person", - "firstName": , - "lastName": - } + the connection handshake. + title: >- + IBC version which can be utilised to determine encodings or protocols + for - If the embedded message type is well-known and has a custom JSON + channels or packets utilising this connection + state: + description: current state of the connection end. + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + counterparty: + description: counterparty chain associated with this connection. + type: object + properties: + client_id: + type: string + description: >- + identifies the client on the counterparty chain associated with a + given - representation, that representation will be embedded adding a field + connection. + connection_id: + type: string + description: >- + identifies the connection end on the counterparty chain associated + with a - `value` which holds the custom JSON in addition to the `@type` + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. - field. Example (for message [google.protobuf.Duration][]): + The constructed key from the Path and the key will be + append(Path.KeyPath, - { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.212s" - } - client_id: + append(Path.KeyPrefix, key...)) + delay_period: type: string - title: client ID associated with the consensus state + format: uint64 + description: delay period associated with this connection. + description: |- + IdentifiedConnection defines a connection with additional connection + identifier field. + ibc.core.connection.v1.MsgConnectionOpenAckResponse: + type: object + description: >- + MsgConnectionOpenAckResponse defines the Msg/ConnectionOpenAck response + type. + ibc.core.connection.v1.MsgConnectionOpenConfirmResponse: + type: object + description: |- + MsgConnectionOpenConfirmResponse defines the Msg/ConnectionOpenConfirm + response type. + ibc.core.connection.v1.MsgConnectionOpenInitResponse: + type: object + description: |- + MsgConnectionOpenInitResponse defines the Msg/ConnectionOpenInit response + type. + ibc.core.connection.v1.MsgConnectionOpenTryResponse: + type: object + description: >- + MsgConnectionOpenTryResponse defines the Msg/ConnectionOpenTry response + type. + ibc.core.connection.v1.Params: + type: object + properties: + max_expected_time_per_block: + type: string + format: uint64 + description: >- + maximum expected time per block (in nanoseconds), used to enforce + block delay. This parameter should reflect the + + largest amount of time that the chain might reasonably take to produce + the next block under normal operating + + conditions. A safe choice is 3-5x the expected time per block. + description: Params defines the set of Connection parameters. + ibc.core.connection.v1.QueryClientConnectionsResponse: + type: object + properties: + connection_paths: + type: array + items: + type: string + description: slice of all the connection paths associated with a client. proof: type: string format: byte title: merkle proof of existence proof_height: - title: height at which the proof was retrieved + title: height at which the proof was generated type: object properties: revision_number: @@ -75537,277 +74432,191 @@ definitions: gets reset title: |- - QueryConnectionConsensusStateResponse is the response type for the - Query/ConnectionConsensusState RPC method - ibc.core.connection.v1.QueryConnectionParamsResponse: + QueryClientConnectionsResponse is the response type for the + Query/ClientConnections RPC method + ibc.core.connection.v1.QueryConnectionClientStateResponse: type: object properties: - params: - description: params defines the parameters of the module. + identified_client_state: + title: client state associated with the channel type: object properties: - max_expected_time_per_block: + client_id: type: string - format: uint64 - description: >- - maximum expected time per block (in nanoseconds), used to enforce - block delay. This parameter should reflect the + title: client identifier + client_state: + title: client state + type: object + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of the + serialized - largest amount of time that the chain might reasonably take to - produce the next block under normal operating + protocol buffer message. This string must contain at least - conditions. A safe choice is 3-5x the expected time per block. - description: >- - QueryConnectionParamsResponse is the response type for the - Query/ConnectionParams RPC method. - ibc.core.connection.v1.QueryConnectionResponse: - type: object - properties: - connection: - title: connection associated with the request identifier - type: object - properties: - client_id: - type: string - description: client associated with this connection. - versions: - type: array - items: - type: object - properties: - identifier: - type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: >- - Version defines the versioning scheme used to negotiate the IBC - verison in + one "/" character. The last segment of the URL's path must + represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a + canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary all + types that they + + expect it to use in the context of Any. However, for URLs + which use the + + scheme `http`, `https`, or no scheme, one can optionally set + up a type + + server that maps type URLs to message definitions as follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on + the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the + official + + protobuf release, and it is not used for type URLs beginning + with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) might + be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message + along with a - the connection handshake. - description: >- - IBC version which can be utilised to determine encodings or - protocols for + URL that describes the type of the serialized message. - channels or packets utilising this connection. - state: - description: current state of the connection end. - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - counterparty: - description: counterparty chain associated with this connection. - type: object - properties: - client_id: - type: string - description: >- - identifies the client on the counterparty chain associated - with a given - connection. - connection_id: - type: string - description: >- - identifies the connection end on the counterparty chain - associated with a + Protobuf library provides support to pack/unpack Any values in the + form - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. + of utility functions or additional generated methods of the Any + type. - The constructed key from the Path and the key will be - append(Path.KeyPath, - append(Path.KeyPrefix, key...)) - delay_period: - type: string - format: uint64 - description: >- - delay period that must pass before a consensus state can be used - for + Example 1: Pack and unpack a message in C++. - packet-verification NOTE: delay period logic is only implemented - by some + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } - clients. - description: >- - ConnectionEnd defines a stateful object on a chain connected to - another + Example 2: Pack and unpack a message in Java. - separate one. + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } - NOTE: there must only be 2 defined ConnectionEnds to establish + Example 3: Pack and unpack a message in Python. - a connection between two chains. - proof: - type: string - format: byte - title: merkle proof of existence - proof_height: - title: height at which the proof was retrieved - type: object - properties: - revision_number: - type: string - format: uint64 - title: the revision that the client is currently on - revision_height: - type: string - format: uint64 - title: the height within the given revision - description: >- - Normally the RevisionHeight is incremented at each height while - keeping + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... - RevisionNumber the same. However some consensus algorithms may choose - to + Example 4: Pack and unpack a message in Go - reset the height in certain conditions e.g. hard forks, state-machine + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } - breaking changes In these cases, the RevisionNumber is incremented so - that + The pack methods provided by protobuf library will by default use - height continues to be monitonically increasing even as the - RevisionHeight + 'type.googleapis.com/full.type.name' as the type URL and the + unpack - gets reset - description: >- - QueryConnectionResponse is the response type for the Query/Connection RPC + methods only use the fully qualified type name after the last '/' - method. Besides the connection end, it includes a proof and the height - from + in the type URL, for example "foo.bar.com/x/y.z" will yield type - which the proof was retrieved. - ibc.core.connection.v1.QueryConnectionsResponse: - type: object - properties: - connections: - type: array - items: - type: object - properties: - id: - type: string - description: connection identifier. - client_id: - type: string - description: client associated with this connection. - versions: - type: array - items: - type: object - properties: - identifier: - type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: >- - Version defines the versioning scheme used to negotiate the - IBC verison in + name "y.z". - the connection handshake. - title: >- - IBC version which can be utilised to determine encodings or - protocols for - channels or packets utilising this connection - state: - description: current state of the connection end. - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - counterparty: - description: counterparty chain associated with this connection. - type: object - properties: - client_id: - type: string - description: >- - identifies the client on the counterparty chain associated - with a given - connection. - connection_id: - type: string - description: >- - identifies the connection end on the counterparty chain - associated with a + JSON - given connection. - prefix: - description: commitment merkle prefix of the counterparty chain. - type: object - properties: - key_prefix: - type: string - format: byte - title: >- - MerklePrefix is merkle path prefixed to the key. + ==== - The constructed key from the Path and the key will be - append(Path.KeyPath, + The JSON representation of an `Any` value uses the regular - append(Path.KeyPrefix, key...)) - delay_period: - type: string - format: uint64 - description: delay period associated with this connection. - description: |- - IdentifiedConnection defines a connection with additional connection - identifier field. - description: list of stored connections of the chain. - pagination: - title: pagination response - type: object - properties: - next_key: - type: string - format: byte - description: |- - next_key is the key to be passed to PageRequest.key to - query the next page most efficiently. It will be empty if - there are no more results. - total: - type: string - format: uint64 - title: >- - total is total number of results available if - PageRequest.count_total + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a + field + + `value` which holds the custom JSON in addition to the `@type` - was set, its value is undefined otherwise - description: |- - PageResponse is to be embedded in gRPC response messages where the - corresponding request message has used PageRequest. + field. Example (for message [google.protobuf.Duration][]): - message SomeResponse { - repeated Bar results = 1; - PageResponse page = 2; - } - height: - title: query block height + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + description: |- + IdentifiedClientState defines a client state with an additional client + identifier field. + proof: + type: string + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved type: object properties: revision_number: @@ -75834,466 +74643,536 @@ definitions: RevisionHeight gets reset - description: >- - QueryConnectionsResponse is the response type for the Query/Connections - RPC - - method. - ibc.core.connection.v1.State: - type: string - enum: - - STATE_UNINITIALIZED_UNSPECIFIED - - STATE_INIT - - STATE_TRYOPEN - - STATE_OPEN - default: STATE_UNINITIALIZED_UNSPECIFIED - description: |- - State defines if a connection is in one of the following states: - INIT, TRYOPEN, OPEN or UNINITIALIZED. - - - STATE_UNINITIALIZED_UNSPECIFIED: Default State - - STATE_INIT: A connection end has just started the opening handshake. - - STATE_TRYOPEN: A connection end has acknowledged the handshake step on the counterparty - chain. - - STATE_OPEN: A connection end has completed the handshake. - ibc.core.connection.v1.Version: - type: object - properties: - identifier: - type: string - title: unique version identifier - features: - type: array - items: - type: string - title: list of features compatible with the specified identifier - description: |- - Version defines the versioning scheme used to negotiate the IBC verison in - the connection handshake. - layer.dispute.DisputeCategory: - type: string - enum: - - DISPUTE_CATEGORY_UNSPECIFIED - - DISPUTE_CATEGORY_WARNING - - DISPUTE_CATEGORY_MINOR - - DISPUTE_CATEGORY_MAJOR - default: DISPUTE_CATEGORY_UNSPECIFIED - description: |2- - - DISPUTE_CATEGORY_UNSPECIFIED: UNSPECIFIED defines an invalid dispute category. - - DISPUTE_CATEGORY_WARNING: WARNING defines a 1 percent slashing. - - DISPUTE_CATEGORY_MINOR: MINOR defines a 5 percent slashing. - - DISPUTE_CATEGORY_MAJOR: MAJOR defines a 100 percent slashing. - layer.dispute.MicroReport: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryId: - type: string - value: - type: string - timestamp: - type: string - format: int64 - layer.dispute.MsgAddFeeToDisputeResponse: - type: object - layer.dispute.MsgProposeDisputeResponse: - type: object - layer.dispute.MsgVoteResponse: - type: object - layer.dispute.Params: - type: object - description: Params defines the parameters for the module. - layer.dispute.QueryParamsResponse: + title: |- + QueryConnectionClientStateResponse is the response type for the + Query/ConnectionClientState RPC method + ibc.core.connection.v1.QueryConnectionConsensusStateResponse: type: object properties: - params: - description: params holds all the parameters of this module. + consensus_state: + title: consensus state associated with the channel type: object - description: QueryParamsResponse is response type for the Query/Params RPC method. - layer.dispute.VoteEnum: - type: string - enum: - - VOTE_INVALID - - VOTE_SUPPORT - - VOTE_AGAINST - default: VOTE_INVALID - layer.oracle.Aggregate: - type: object - properties: - queryId: - type: string - aggregateValue: - type: string - aggregateReporter: - type: string - reporterPower: - type: string - format: int64 - standardDeviation: - type: number - format: double - reporters: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - flagged: - type: boolean - nonce: - type: string - format: int64 - aggregateReportIndex: - type: string - format: int64 - layer.oracle.AggregateReporter: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - layer.oracle.MicroReport: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: - type: string - value: - type: string - blockNumber: + properties: + '@type': + type: string + description: >- + A URL/resource name that uniquely identifies the type of the + serialized + + protocol buffer message. This string must contain at least + + one "/" character. The last segment of the URL's path must + represent + + the fully qualified name of the type (as in + + `path/google.protobuf.Duration`). The name should be in a + canonical form + + (e.g., leading "." is not accepted). + + + In practice, teams usually precompile into the binary all types + that they + + expect it to use in the context of Any. However, for URLs which + use the + + scheme `http`, `https`, or no scheme, one can optionally set up a + type + + server that maps type URLs to message definitions as follows: + + + * If no scheme is provided, `https` is assumed. + + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the + official + + protobuf release, and it is not used for type URLs beginning with + + type.googleapis.com. + + + Schemes other than `http`, `https` (or the empty scheme) might be + + used with implementation specific semantics. + additionalProperties: {} + description: >- + `Any` contains an arbitrary serialized protocol buffer message along + with a + + URL that describes the type of the serialized message. + + + Protobuf library provides support to pack/unpack Any values in the + form + + of utility functions or additional generated methods of the Any type. + + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + + 'type.googleapis.com/full.type.name' as the type URL and the unpack + + methods only use the fully qualified type name after the last '/' + + in the type URL, for example "foo.bar.com/x/y.z" will yield type + + name "y.z". + + + + JSON + + ==== + + The JSON representation of an `Any` value uses the regular + + representation of the deserialized, embedded message, with an + + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + + representation, that representation will be embedded adding a field + + `value` which holds the custom JSON in addition to the `@type` + + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + client_id: type: string - format: int64 - timestamp: + title: client ID associated with the consensus state + proof: type: string - format: date-time - layer.oracle.MsgCommitReportResponse: - type: object - layer.oracle.MsgSubmitValueResponse: - type: object - layer.oracle.MsgTipResponse: - type: object - layer.oracle.MsgUpdateParamsResponse: - type: object - layer.oracle.Params: + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved + type: object + properties: + revision_number: + type: string + format: uint64 + title: the revision that the client is currently on + revision_height: + type: string + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping + + RevisionNumber the same. However some consensus algorithms may choose + to + + reset the height in certain conditions e.g. hard forks, state-machine + + breaking changes In these cases, the RevisionNumber is incremented so + that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + title: |- + QueryConnectionConsensusStateResponse is the response type for the + Query/ConnectionConsensusState RPC method + ibc.core.connection.v1.QueryConnectionParamsResponse: type: object properties: - cycle_list: - type: array - items: - type: string - description: Params defines the parameters for the module. - layer.oracle.QueryGetAggregatedReportResponse: + params: + description: params defines the parameters of the module. + type: object + properties: + max_expected_time_per_block: + type: string + format: uint64 + description: >- + maximum expected time per block (in nanoseconds), used to enforce + block delay. This parameter should reflect the + + largest amount of time that the chain might reasonably take to + produce the next block under normal operating + + conditions. A safe choice is 3-5x the expected time per block. + description: >- + QueryConnectionParamsResponse is the response type for the + Query/ConnectionParams RPC method. + ibc.core.connection.v1.QueryConnectionResponse: type: object properties: - report: + connection: + title: connection associated with the request identifier type: object properties: - queryId: - type: string - aggregateValue: - type: string - aggregateReporter: - type: string - reporterPower: + client_id: type: string - format: int64 - standardDeviation: - type: number - format: double - reporters: + description: client associated with this connection. + versions: type: array items: type: object properties: - reporter: - type: string - power: + identifier: type: string - format: int64 - flagged: - type: boolean - nonce: - type: string - format: int64 - aggregateReportIndex: - type: string - format: int64 - layer.oracle.QueryGetCurrentTipResponse: - type: object - properties: - tips: - type: object - properties: - queryData: + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: >- + Version defines the versioning scheme used to negotiate the IBC + verison in + + the connection handshake. + description: >- + IBC version which can be utilised to determine encodings or + protocols for + + channels or packets utilising this connection. + state: + description: current state of the connection end. type: string - amount: + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + counterparty: + description: counterparty chain associated with this connection. type: object properties: - denom: + client_id: type: string - amount: + description: >- + identifies the client on the counterparty chain associated + with a given + + connection. + connection_id: type: string - description: >- - Coin defines a token with a denomination and an amount. + description: >- + identifies the connection end on the counterparty chain + associated with a + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. - NOTE: The amount field is an Int which implements the custom - method + The constructed key from the Path and the key will be + append(Path.KeyPath, - signatures required by gogoproto. - totalTips: - type: object - properties: - denom: - type: string - amount: - type: string + append(Path.KeyPrefix, key...)) + delay_period: + type: string + format: uint64 description: >- - Coin defines a token with a denomination and an amount. + delay period that must pass before a consensus state can be used + for + packet-verification NOTE: delay period logic is only implemented + by some - NOTE: The amount field is an Int which implements the custom - method + clients. + description: >- + ConnectionEnd defines a stateful object on a chain connected to + another - signatures required by gogoproto. - layer.oracle.QueryGetReportsbyQidResponse: - type: object - properties: - reports: - type: object - properties: - microReports: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: - type: string - value: - type: string - blockNumber: - type: string - format: int64 - timestamp: - type: string - format: date-time - layer.oracle.QueryGetReportsbyReporterResponse: - type: object - properties: - microReports: - type: array - items: - type: object - properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: - type: string - value: - type: string - blockNumber: - type: string - format: int64 - timestamp: - type: string - format: date-time - layer.oracle.QueryGetTimeBasedRewardsResponse: - type: object - properties: - reward: + separate one. + + NOTE: there must only be 2 defined ConnectionEnds to establish + + a connection between two chains. + proof: + type: string + format: byte + title: merkle proof of existence + proof_height: + title: height at which the proof was retrieved type: object properties: - denom: + revision_number: type: string - amount: + format: uint64 + title: the revision that the client is currently on + revision_height: type: string - description: |- - Coin defines a token with a denomination and an amount. + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping + + RevisionNumber the same. However some consensus algorithms may choose + to + + reset the height in certain conditions e.g. hard forks, state-machine - NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - layer.oracle.QueryGetUserTipTotalResponse: - type: object - properties: - totalTips: - type: object - properties: - address: - type: string - total: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. + breaking changes In these cases, the RevisionNumber is incremented so + that + height continues to be monitonically increasing even as the + RevisionHeight - NOTE: The amount field is an Int which implements the custom - method + gets reset + description: >- + QueryConnectionResponse is the response type for the Query/Connection RPC - signatures required by gogoproto. - layer.oracle.QueryParamsResponse: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - properties: - cycle_list: - type: array - items: - type: string - description: QueryParamsResponse is response type for the Query/Params RPC method. - layer.oracle.Reports: + method. Besides the connection end, it includes a proof and the height + from + + which the proof was retrieved. + ibc.core.connection.v1.QueryConnectionsResponse: type: object properties: - microReports: + connections: type: array items: type: object properties: - reporter: - type: string - power: - type: string - format: int64 - queryType: - type: string - queryId: - type: string - aggregateMethod: + id: type: string - value: + description: connection identifier. + client_id: type: string - blockNumber: + description: client associated with this connection. + versions: + type: array + items: + type: object + properties: + identifier: + type: string + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: >- + Version defines the versioning scheme used to negotiate the + IBC verison in + + the connection handshake. + title: >- + IBC version which can be utilised to determine encodings or + protocols for + + channels or packets utilising this connection + state: + description: current state of the connection end. type: string - format: int64 - timestamp: + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + counterparty: + description: counterparty chain associated with this connection. + type: object + properties: + client_id: + type: string + description: >- + identifies the client on the counterparty chain associated + with a given + + connection. + connection_id: + type: string + description: >- + identifies the connection end on the counterparty chain + associated with a + + given connection. + prefix: + description: commitment merkle prefix of the counterparty chain. + type: object + properties: + key_prefix: + type: string + format: byte + title: >- + MerklePrefix is merkle path prefixed to the key. + + The constructed key from the Path and the key will be + append(Path.KeyPath, + + append(Path.KeyPrefix, key...)) + delay_period: type: string - format: date-time - layer.oracle.Tips: - type: object - properties: - queryData: - type: string - amount: + format: uint64 + description: delay period associated with this connection. + description: |- + IdentifiedConnection defines a connection with additional connection + identifier field. + description: list of stored connections of the chain. + pagination: + title: pagination response type: object properties: - denom: + next_key: type: string - amount: + format: byte + description: |- + next_key is the key to be passed to PageRequest.key to + query the next page most efficiently. It will be empty if + there are no more results. + total: type: string - description: |- - Coin defines a token with a denomination and an amount. + format: uint64 + title: >- + total is total number of results available if + PageRequest.count_total - NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - totalTips: - type: object - properties: - denom: - type: string - amount: - type: string + was set, its value is undefined otherwise description: |- - Coin defines a token with a denomination and an amount. + PageResponse is to be embedded in gRPC response messages where the + corresponding request message has used PageRequest. - NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - layer.oracle.UserTipTotal: - type: object - properties: - address: - type: string - total: + message SomeResponse { + repeated Bar results = 1; + PageResponse page = 2; + } + height: + title: query block height type: object properties: - denom: + revision_number: type: string - amount: + format: uint64 + title: the revision that the client is currently on + revision_height: type: string - description: |- - Coin defines a token with a denomination and an amount. + format: uint64 + title: the height within the given revision + description: >- + Normally the RevisionHeight is incremented at each height while + keeping - NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - layer.registry.DataSpec: - type: object - properties: - documentHash: - type: string - valueType: - type: string - aggregationMethod: - type: string - layer.registry.MsgRegisterQueryResponse: - type: object - properties: - queryId: - type: string - layer.registry.MsgRegisterSpecResponse: - type: object - layer.registry.Params: - type: object - description: Params defines the parameters for the module. - layer.registry.QueryGetDataSpecResponse: - type: object - properties: - spec: - type: object - properties: - documentHash: - type: string - valueType: - type: string - aggregationMethod: - type: string - layer.registry.QueryGetQueryDataResponse: + RevisionNumber the same. However some consensus algorithms may choose + to + + reset the height in certain conditions e.g. hard forks, state-machine + + breaking changes In these cases, the RevisionNumber is incremented so + that + + height continues to be monitonically increasing even as the + RevisionHeight + + gets reset + description: >- + QueryConnectionsResponse is the response type for the Query/Connections + RPC + + method. + ibc.core.connection.v1.State: + type: string + enum: + - STATE_UNINITIALIZED_UNSPECIFIED + - STATE_INIT + - STATE_TRYOPEN + - STATE_OPEN + default: STATE_UNINITIALIZED_UNSPECIFIED + description: |- + State defines if a connection is in one of the following states: + INIT, TRYOPEN, OPEN or UNINITIALIZED. + + - STATE_UNINITIALIZED_UNSPECIFIED: Default State + - STATE_INIT: A connection end has just started the opening handshake. + - STATE_TRYOPEN: A connection end has acknowledged the handshake step on the counterparty + chain. + - STATE_OPEN: A connection end has completed the handshake. + ibc.core.connection.v1.Version: type: object properties: - queryData: + identifier: type: string - layer.registry.QueryParamsResponse: - type: object - properties: - params: - description: params holds all the parameters of this module. - type: object - description: QueryParamsResponse is response type for the Query/Params RPC method. + title: unique version identifier + features: + type: array + items: + type: string + title: list of features compatible with the specified identifier + description: |- + Version defines the versioning scheme used to negotiate the IBC verison in + the connection handshake. diff --git a/lib/prices/utils.go b/lib/prices/utils.go index 2ffcd0767..4f600011e 100644 --- a/lib/prices/utils.go +++ b/lib/prices/utils.go @@ -1,7 +1,12 @@ package prices import ( + "encoding/hex" + "fmt" "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/shopspring/decimal" "github.com/tellor-io/layer/daemons/pricefeed/client/types" @@ -99,3 +104,37 @@ func PriceToFloat32ForLogging(price uint64, exponent types.Exponent) float32 { priceFloat32, _ := decimal.NewFromBigInt(new(big.Int).SetUint64(price), exponent).BigFloat().Float32() return priceFloat32 } + +func EncodePrice(number float64, exponent int32) (string, error) { + newExponent := 18 + exponent + strExponent := fmt.Sprintf("%d", newExponent) + + formatString := "%." + strExponent + "f" + strNumber := fmt.Sprintf(formatString, number) + + parts := strings.Split(strNumber, ".") + if len(parts[1]) > int(newExponent) { + parts[1] = parts[1][:newExponent] + } + truncatedStr := parts[0] + parts[1] + + bigIntNumber := new(big.Int) + _, ok := bigIntNumber.SetString(truncatedStr, 10) + if !ok { + return "", fmt.Errorf("error converting string to big int") + } + + uint256ABIType, err := abi.NewType("uint256", "", nil) + if err != nil { + return "", fmt.Errorf("error creating uint256 abi type, %v", err) + } + + arguments := abi.Arguments{{Type: uint256ABIType}} + encodedBytes, err := arguments.Pack(bigIntNumber) + if err != nil { + return "", fmt.Errorf("error packing arguments, %v", err) + } + + encodedString := hex.EncodeToString(encodedBytes) + return encodedString, nil +} diff --git a/proto/layer/daemons/median_values.proto b/proto/layer/daemons/median_values.proto index 639bf9291..ddc2e6c45 100644 --- a/proto/layer/daemons/median_values.proto +++ b/proto/layer/daemons/median_values.proto @@ -9,20 +9,39 @@ option go_package = "github.com/tellor-io/layer/daemons/server/types"; // MedianValuesService is the service that provides median values for markets service MedianValuesService { - // GetMedianValues returns the median values for all markets - rpc GetMedianValues(GetMedianValuesRequest) returns (GetMedianValuesResponse) { - option (google.api.http).get = "/layer/api/get_median_values"; + // GetAllMedianValues returns the median values for all markets + rpc GetAllMedianValues(GetAllMedianValuesRequest) returns (GetAllMedianValuesResponse) { + option (google.api.http).get = "/layer/api/get_all_median_values"; + } + // GetMedianValue returns the median value for a market given a query data that a market is associated with + rpc GetMedianValue(GetMedianValueRequest) returns (GetMedianValueResponse) { + option (google.api.http).get = "/layer/api/get_median_value/{query_data}"; } } + // MedianValues is the median value for a market message MedianValues { // marketId is the market id for a pair uint32 market_id = 1; uint64 price = 2; + int32 exponent = 3; } -// GetMedianValuesRequest is the request for the GetMedianValues rpc -message GetMedianValuesRequest {} -// GetMedianValuesResponse is the response for the GetMedianValues rpc -message GetMedianValuesResponse { + +// GetAllMedianValuesRequest is the request for the GetAllMedianValues rpc +message GetAllMedianValuesRequest {} + +// GetAllMedianValuesResponse is the response for the GetAllMedianValues rpc +message GetAllMedianValuesResponse { repeated MedianValues median_values = 1; } + +// GetMedianValuesRequest is the request for the GetMedianValues rpc +message GetMedianValueRequest { + // query data to fetch prices for + string query_data = 1; +} + +// GetMedianValueResponse is the response for the GetMedianValue rpc +message GetMedianValueResponse { + MedianValues median_values = 1; +} diff --git a/x/oracle/client/cli/query.go b/x/oracle/client/cli/query.go index fe9db3a28..bdebd670f 100644 --- a/x/oracle/client/cli/query.go +++ b/x/oracle/client/cli/query.go @@ -37,7 +37,9 @@ func GetQueryCmd(queryRoute string) *cobra.Command { cmd.AddCommand(CmdGetTimeBasedRewards()) - cmd.AddCommand(CmdGetMedianValues()) + cmd.AddCommand(CmdGetAllMedianValues()) + + cmd.AddCommand(CmdGetMedianValue()) // this line is used by starport scaffolding # 1 diff --git a/x/oracle/client/cli/query_all_get_median.go b/x/oracle/client/cli/query_all_get_median.go new file mode 100644 index 000000000..c66e88052 --- /dev/null +++ b/x/oracle/client/cli/query_all_get_median.go @@ -0,0 +1,42 @@ +package cli + +import ( + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/cobra" + mediantypes "github.com/tellor-io/layer/daemons/server/types" +) + +var _ = strconv.Itoa(0) + +func CmdGetAllMedianValues() *cobra.Command { + cmd := &cobra.Command{ + Use: "get-all-median-values", + Short: "Query getAllMedianValues", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) (err error) { + + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + queryClient := mediantypes.NewMedianValuesServiceClient(clientCtx) + + params := &mediantypes.GetAllMedianValuesRequest{} + + res, err := queryClient.GetAllMedianValues(cmd.Context(), params) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/oracle/client/cli/query_get_median.go b/x/oracle/client/cli/query_get_median.go index b4d0c563f..f3066dd6a 100644 --- a/x/oracle/client/cli/query_get_median.go +++ b/x/oracle/client/cli/query_get_median.go @@ -11,12 +11,13 @@ import ( var _ = strconv.Itoa(0) -func CmdGetMedianValues() *cobra.Command { +func CmdGetMedianValue() *cobra.Command { cmd := &cobra.Command{ - Use: "get-median-values", - Short: "Query getMedianValues", - Args: cobra.ExactArgs(0), + Use: "get-median-value", + Short: "Query getMedianValue", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { + argQueryData := args[0] clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { @@ -25,9 +26,9 @@ func CmdGetMedianValues() *cobra.Command { queryClient := mediantypes.NewMedianValuesServiceClient(clientCtx) - params := &mediantypes.GetMedianValuesRequest{} + params := &mediantypes.GetMedianValueRequest{QueryData: argQueryData} - res, err := queryClient.GetMedianValues(cmd.Context(), params) + res, err := queryClient.GetMedianValue(cmd.Context(), params) if err != nil { return err } diff --git a/x/oracle/client/cli/tx_commit_submit_report.go b/x/oracle/client/cli/tx_commit_submit_report.go index 63a10d60b..b1c5c4ca6 100644 --- a/x/oracle/client/cli/tx_commit_submit_report.go +++ b/x/oracle/client/cli/tx_commit_submit_report.go @@ -3,58 +3,20 @@ package cli import ( "encoding/hex" "fmt" - "math/big" "strconv" - "strings" "time" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/spf13/cobra" mediantypes "github.com/tellor-io/layer/daemons/server/types" + "github.com/tellor-io/layer/lib/prices" "github.com/tellor-io/layer/x/oracle/types" ) var _ = strconv.Itoa(0) -var ( - queryDataIdMap = map[string]uint32{ - "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003627463000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000": 0, - "00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003657468000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000": 1, - } -) - -func EncodeValue(number float64) (string, error) { - strNumber := fmt.Sprintf("%.18f", number) - - parts := strings.Split(strNumber, ".") - if len(parts[1]) > 18 { - parts[1] = parts[1][:18] - } - truncatedStr := parts[0] + parts[1] - - bigIntNumber := new(big.Int) - _, ok := bigIntNumber.SetString(truncatedStr, 10) - if !ok { - return "", fmt.Errorf("error converting string to big int") - } - - uint256ABIType, err := abi.NewType("uint256", "", nil) - if err != nil { - return "", fmt.Errorf("error creating uint256 abi type, %v", err) - } - - arguments := abi.Arguments{{Type: uint256ABIType}} - encodedBytes, err := arguments.Pack(bigIntNumber) - if err != nil { - return "", fmt.Errorf("error packing arguments, %v", err) - } - - encodedString := hex.EncodeToString(encodedBytes) - return encodedString, nil -} func CmdCommitSubmitReport() *cobra.Command { cmd := &cobra.Command{ Use: "commit-submit-report [query-data]", @@ -69,27 +31,24 @@ func CmdCommitSubmitReport() *cobra.Command { } queryClient := mediantypes.NewMedianValuesServiceClient(clientCtx) - params := &mediantypes.GetMedianValuesRequest{} + params := &mediantypes.GetMedianValueRequest{QueryData: argQueryData} - medianValue, err := queryClient.GetMedianValues(cmd.Context(), params) + medianValue, err := queryClient.GetMedianValue(cmd.Context(), params) if err != nil { return err } - if len(medianValue.MedianValues) == 0 { - return fmt.Errorf("no median values found") + + if medianValue == nil { + return fmt.Errorf("no median values found for query data: %s", argQueryData) } + + fmt.Println("Spot Price:", medianValue.MedianValues.Price) var hexValue string - for _, value := range medianValue.MedianValues { - if queryDataIdMap[argQueryData] == value.MarketId { - fmt.Println("Spot Price:", value.Price) - hexValue, err = EncodeValue(float64(value.Price)) - if err != nil { - return err - } - break - } - return fmt.Errorf("query data not found in median values") + hexValue, err = prices.EncodePrice(float64(medianValue.MedianValues.Price), medianValue.MedianValues.Exponent) + if err != nil { + return err } + fmt.Println("Hex Value:", hexValue) valueDecoded, err := hex.DecodeString(hexValue) if err != nil { return err From d9aa8b04de78fda43fa13b569038793d1d06a96f Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 24 Jan 2024 15:51:18 -0500 Subject: [PATCH 09/10] merge fixes --- api/layer/daemons/median_values.pulsar.go | 2608 +++++++++++++++++ api/layer/daemons/median_values_grpc.pb.go | 141 + api/layer/daemons/pricefeed.pulsar.go | 2293 +++++++++++++++ api/layer/daemons/pricefeed_grpc.pb.go | 103 + daemons/flags/flags_test.go | 2 +- daemons/metrics/client/client.go | 4 +- daemons/mocks/AppOptions.go | 41 + daemons/mocks/BankKeeper.go | 194 ++ daemons/mocks/DistrKeeper.go | 79 + daemons/mocks/ExchangeQueryHandler.go | 81 + daemons/mocks/ExchangeToMarketPrices.go | 74 + daemons/mocks/GrpcClient.go | 91 + daemons/mocks/Logger.go | 94 + daemons/mocks/OracleKeeper.go | 57 + .../mocks/PriceFeedMutableMarketConfigs.go | 108 + daemons/mocks/QueryClient.go | 69 + daemons/mocks/RequestHandler.go | 53 + daemons/mocks/SlashingKeeper.go | 65 + daemons/mocks/StakingKeeper.go | 292 ++ daemons/mocks/TimeProvider.go | 43 + daemons/pricefeed/client/client.go | 5 +- daemons/pricefeed/client/client_test.go | 4 +- .../client/price_encoder/price_encoder.go | 4 +- .../price_encoder/price_encoder_test.go | 8 +- .../price_fetcher/mutable_state_test.go | 6 +- .../client/price_fetcher/price_fetcher.go | 4 +- .../price_fetcher/price_fetcher_test.go | 4 +- .../exchange_query_handler_test.go | 2 +- daemons/pricefeed/client/subtask_runner.go | 2 +- .../price_feed_mutable_market_configs.go | 2 +- daemons/pricefeed/metrics/metrics.go | 2 +- daemons/server/median/median.go | 2 +- daemons/server/pricefeed.go | 2 +- daemons/server/server.go | 4 +- daemons/server/types/health_checker.go | 2 +- daemons/server/types/health_monitor.go | 5 +- daemons/server/types/median_values.pb.gw.go | 4 +- .../types/pricefeed/exchange_to_price.go | 2 +- .../pricefeed/market_to_exchange_prices.go | 2 +- daemons/types/health_checkable.go | 2 +- docs/static/openapi.yml | 192 ++ go.mod | 11 +- go.sum | 15 +- lib/metrics/lib.go | 2 +- lib/metrics/util.go | 2 +- lib/metrics/util_test.go | 2 +- testutil/encoding/utils.go | 23 +- testutil/grpc/grpc.go | 2 +- testutil/grpc/tcp.go | 2 +- .../client/cli/tx_commit_submit_report.go | 3 +- 50 files changed, 6746 insertions(+), 68 deletions(-) create mode 100644 api/layer/daemons/median_values.pulsar.go create mode 100644 api/layer/daemons/median_values_grpc.pb.go create mode 100644 api/layer/daemons/pricefeed.pulsar.go create mode 100644 api/layer/daemons/pricefeed_grpc.pb.go create mode 100644 daemons/mocks/AppOptions.go create mode 100644 daemons/mocks/BankKeeper.go create mode 100644 daemons/mocks/DistrKeeper.go create mode 100644 daemons/mocks/ExchangeQueryHandler.go create mode 100644 daemons/mocks/ExchangeToMarketPrices.go create mode 100644 daemons/mocks/GrpcClient.go create mode 100644 daemons/mocks/Logger.go create mode 100644 daemons/mocks/OracleKeeper.go create mode 100644 daemons/mocks/PriceFeedMutableMarketConfigs.go create mode 100644 daemons/mocks/QueryClient.go create mode 100644 daemons/mocks/RequestHandler.go create mode 100644 daemons/mocks/SlashingKeeper.go create mode 100644 daemons/mocks/StakingKeeper.go create mode 100644 daemons/mocks/TimeProvider.go diff --git a/api/layer/daemons/median_values.pulsar.go b/api/layer/daemons/median_values.pulsar.go new file mode 100644 index 000000000..ebefb1960 --- /dev/null +++ b/api/layer/daemons/median_values.pulsar.go @@ -0,0 +1,2608 @@ +// Code generated by protoc-gen-go-pulsar. DO NOT EDIT. +package daemons + +import ( + fmt "fmt" + runtime "github.com/cosmos/cosmos-proto/runtime" + _ "github.com/cosmos/gogoproto/gogoproto" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoiface "google.golang.org/protobuf/runtime/protoiface" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + io "io" + reflect "reflect" + sync "sync" +) + +var ( + md_MedianValues protoreflect.MessageDescriptor + fd_MedianValues_market_id protoreflect.FieldDescriptor + fd_MedianValues_price protoreflect.FieldDescriptor + fd_MedianValues_exponent protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_median_values_proto_init() + md_MedianValues = File_layer_daemons_median_values_proto.Messages().ByName("MedianValues") + fd_MedianValues_market_id = md_MedianValues.Fields().ByName("market_id") + fd_MedianValues_price = md_MedianValues.Fields().ByName("price") + fd_MedianValues_exponent = md_MedianValues.Fields().ByName("exponent") +} + +var _ protoreflect.Message = (*fastReflection_MedianValues)(nil) + +type fastReflection_MedianValues MedianValues + +func (x *MedianValues) ProtoReflect() protoreflect.Message { + return (*fastReflection_MedianValues)(x) +} + +func (x *MedianValues) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_median_values_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_MedianValues_messageType fastReflection_MedianValues_messageType +var _ protoreflect.MessageType = fastReflection_MedianValues_messageType{} + +type fastReflection_MedianValues_messageType struct{} + +func (x fastReflection_MedianValues_messageType) Zero() protoreflect.Message { + return (*fastReflection_MedianValues)(nil) +} +func (x fastReflection_MedianValues_messageType) New() protoreflect.Message { + return new(fastReflection_MedianValues) +} +func (x fastReflection_MedianValues_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MedianValues +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_MedianValues) Descriptor() protoreflect.MessageDescriptor { + return md_MedianValues +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_MedianValues) Type() protoreflect.MessageType { + return _fastReflection_MedianValues_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_MedianValues) New() protoreflect.Message { + return new(fastReflection_MedianValues) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_MedianValues) Interface() protoreflect.ProtoMessage { + return (*MedianValues)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_MedianValues) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.MarketId != uint32(0) { + value := protoreflect.ValueOfUint32(x.MarketId) + if !f(fd_MedianValues_market_id, value) { + return + } + } + if x.Price != uint64(0) { + value := protoreflect.ValueOfUint64(x.Price) + if !f(fd_MedianValues_price, value) { + return + } + } + if x.Exponent != int32(0) { + value := protoreflect.ValueOfInt32(x.Exponent) + if !f(fd_MedianValues_exponent, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_MedianValues) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.MedianValues.market_id": + return x.MarketId != uint32(0) + case "layer.daemons.MedianValues.price": + return x.Price != uint64(0) + case "layer.daemons.MedianValues.exponent": + return x.Exponent != int32(0) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MedianValues")) + } + panic(fmt.Errorf("message layer.daemons.MedianValues does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MedianValues) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.MedianValues.market_id": + x.MarketId = uint32(0) + case "layer.daemons.MedianValues.price": + x.Price = uint64(0) + case "layer.daemons.MedianValues.exponent": + x.Exponent = int32(0) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MedianValues")) + } + panic(fmt.Errorf("message layer.daemons.MedianValues does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_MedianValues) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.MedianValues.market_id": + value := x.MarketId + return protoreflect.ValueOfUint32(value) + case "layer.daemons.MedianValues.price": + value := x.Price + return protoreflect.ValueOfUint64(value) + case "layer.daemons.MedianValues.exponent": + value := x.Exponent + return protoreflect.ValueOfInt32(value) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MedianValues")) + } + panic(fmt.Errorf("message layer.daemons.MedianValues does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MedianValues) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.MedianValues.market_id": + x.MarketId = uint32(value.Uint()) + case "layer.daemons.MedianValues.price": + x.Price = value.Uint() + case "layer.daemons.MedianValues.exponent": + x.Exponent = int32(value.Int()) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MedianValues")) + } + panic(fmt.Errorf("message layer.daemons.MedianValues does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MedianValues) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.MedianValues.market_id": + panic(fmt.Errorf("field market_id of message layer.daemons.MedianValues is not mutable")) + case "layer.daemons.MedianValues.price": + panic(fmt.Errorf("field price of message layer.daemons.MedianValues is not mutable")) + case "layer.daemons.MedianValues.exponent": + panic(fmt.Errorf("field exponent of message layer.daemons.MedianValues is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MedianValues")) + } + panic(fmt.Errorf("message layer.daemons.MedianValues does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_MedianValues) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.MedianValues.market_id": + return protoreflect.ValueOfUint32(uint32(0)) + case "layer.daemons.MedianValues.price": + return protoreflect.ValueOfUint64(uint64(0)) + case "layer.daemons.MedianValues.exponent": + return protoreflect.ValueOfInt32(int32(0)) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MedianValues")) + } + panic(fmt.Errorf("message layer.daemons.MedianValues does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_MedianValues) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.MedianValues", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_MedianValues) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MedianValues) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_MedianValues) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_MedianValues) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*MedianValues) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if x.MarketId != 0 { + n += 1 + runtime.Sov(uint64(x.MarketId)) + } + if x.Price != 0 { + n += 1 + runtime.Sov(uint64(x.Price)) + } + if x.Exponent != 0 { + n += 1 + runtime.Sov(uint64(x.Exponent)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*MedianValues) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if x.Exponent != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.Exponent)) + i-- + dAtA[i] = 0x18 + } + if x.Price != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.Price)) + i-- + dAtA[i] = 0x10 + } + if x.MarketId != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.MarketId)) + i-- + dAtA[i] = 0x8 + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*MedianValues) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MedianValues: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MedianValues: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MarketId", wireType) + } + x.MarketId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.MarketId |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) + } + x.Price = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.Price |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Exponent", wireType) + } + x.Exponent = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.Exponent |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_GetAllMedianValuesRequest protoreflect.MessageDescriptor +) + +func init() { + file_layer_daemons_median_values_proto_init() + md_GetAllMedianValuesRequest = File_layer_daemons_median_values_proto.Messages().ByName("GetAllMedianValuesRequest") +} + +var _ protoreflect.Message = (*fastReflection_GetAllMedianValuesRequest)(nil) + +type fastReflection_GetAllMedianValuesRequest GetAllMedianValuesRequest + +func (x *GetAllMedianValuesRequest) ProtoReflect() protoreflect.Message { + return (*fastReflection_GetAllMedianValuesRequest)(x) +} + +func (x *GetAllMedianValuesRequest) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_median_values_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_GetAllMedianValuesRequest_messageType fastReflection_GetAllMedianValuesRequest_messageType +var _ protoreflect.MessageType = fastReflection_GetAllMedianValuesRequest_messageType{} + +type fastReflection_GetAllMedianValuesRequest_messageType struct{} + +func (x fastReflection_GetAllMedianValuesRequest_messageType) Zero() protoreflect.Message { + return (*fastReflection_GetAllMedianValuesRequest)(nil) +} +func (x fastReflection_GetAllMedianValuesRequest_messageType) New() protoreflect.Message { + return new(fastReflection_GetAllMedianValuesRequest) +} +func (x fastReflection_GetAllMedianValuesRequest_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_GetAllMedianValuesRequest +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_GetAllMedianValuesRequest) Descriptor() protoreflect.MessageDescriptor { + return md_GetAllMedianValuesRequest +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_GetAllMedianValuesRequest) Type() protoreflect.MessageType { + return _fastReflection_GetAllMedianValuesRequest_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_GetAllMedianValuesRequest) New() protoreflect.Message { + return new(fastReflection_GetAllMedianValuesRequest) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_GetAllMedianValuesRequest) Interface() protoreflect.ProtoMessage { + return (*GetAllMedianValuesRequest)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_GetAllMedianValuesRequest) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_GetAllMedianValuesRequest) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesRequest does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesRequest) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesRequest does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_GetAllMedianValuesRequest) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesRequest does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesRequest) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesRequest does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesRequest) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesRequest does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_GetAllMedianValuesRequest) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesRequest does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_GetAllMedianValuesRequest) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.GetAllMedianValuesRequest", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_GetAllMedianValuesRequest) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesRequest) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_GetAllMedianValuesRequest) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_GetAllMedianValuesRequest) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*GetAllMedianValuesRequest) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*GetAllMedianValuesRequest) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*GetAllMedianValuesRequest) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetAllMedianValuesRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetAllMedianValuesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var _ protoreflect.List = (*_GetAllMedianValuesResponse_1_list)(nil) + +type _GetAllMedianValuesResponse_1_list struct { + list *[]*MedianValues +} + +func (x *_GetAllMedianValuesResponse_1_list) Len() int { + if x.list == nil { + return 0 + } + return len(*x.list) +} + +func (x *_GetAllMedianValuesResponse_1_list) Get(i int) protoreflect.Value { + return protoreflect.ValueOfMessage((*x.list)[i].ProtoReflect()) +} + +func (x *_GetAllMedianValuesResponse_1_list) Set(i int, value protoreflect.Value) { + valueUnwrapped := value.Message() + concreteValue := valueUnwrapped.Interface().(*MedianValues) + (*x.list)[i] = concreteValue +} + +func (x *_GetAllMedianValuesResponse_1_list) Append(value protoreflect.Value) { + valueUnwrapped := value.Message() + concreteValue := valueUnwrapped.Interface().(*MedianValues) + *x.list = append(*x.list, concreteValue) +} + +func (x *_GetAllMedianValuesResponse_1_list) AppendMutable() protoreflect.Value { + v := new(MedianValues) + *x.list = append(*x.list, v) + return protoreflect.ValueOfMessage(v.ProtoReflect()) +} + +func (x *_GetAllMedianValuesResponse_1_list) Truncate(n int) { + for i := n; i < len(*x.list); i++ { + (*x.list)[i] = nil + } + *x.list = (*x.list)[:n] +} + +func (x *_GetAllMedianValuesResponse_1_list) NewElement() protoreflect.Value { + v := new(MedianValues) + return protoreflect.ValueOfMessage(v.ProtoReflect()) +} + +func (x *_GetAllMedianValuesResponse_1_list) IsValid() bool { + return x.list != nil +} + +var ( + md_GetAllMedianValuesResponse protoreflect.MessageDescriptor + fd_GetAllMedianValuesResponse_median_values protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_median_values_proto_init() + md_GetAllMedianValuesResponse = File_layer_daemons_median_values_proto.Messages().ByName("GetAllMedianValuesResponse") + fd_GetAllMedianValuesResponse_median_values = md_GetAllMedianValuesResponse.Fields().ByName("median_values") +} + +var _ protoreflect.Message = (*fastReflection_GetAllMedianValuesResponse)(nil) + +type fastReflection_GetAllMedianValuesResponse GetAllMedianValuesResponse + +func (x *GetAllMedianValuesResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_GetAllMedianValuesResponse)(x) +} + +func (x *GetAllMedianValuesResponse) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_median_values_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_GetAllMedianValuesResponse_messageType fastReflection_GetAllMedianValuesResponse_messageType +var _ protoreflect.MessageType = fastReflection_GetAllMedianValuesResponse_messageType{} + +type fastReflection_GetAllMedianValuesResponse_messageType struct{} + +func (x fastReflection_GetAllMedianValuesResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_GetAllMedianValuesResponse)(nil) +} +func (x fastReflection_GetAllMedianValuesResponse_messageType) New() protoreflect.Message { + return new(fastReflection_GetAllMedianValuesResponse) +} +func (x fastReflection_GetAllMedianValuesResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_GetAllMedianValuesResponse +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_GetAllMedianValuesResponse) Descriptor() protoreflect.MessageDescriptor { + return md_GetAllMedianValuesResponse +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_GetAllMedianValuesResponse) Type() protoreflect.MessageType { + return _fastReflection_GetAllMedianValuesResponse_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_GetAllMedianValuesResponse) New() protoreflect.Message { + return new(fastReflection_GetAllMedianValuesResponse) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_GetAllMedianValuesResponse) Interface() protoreflect.ProtoMessage { + return (*GetAllMedianValuesResponse)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_GetAllMedianValuesResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if len(x.MedianValues) != 0 { + value := protoreflect.ValueOfList(&_GetAllMedianValuesResponse_1_list{list: &x.MedianValues}) + if !f(fd_GetAllMedianValuesResponse_median_values, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_GetAllMedianValuesResponse) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.GetAllMedianValuesResponse.median_values": + return len(x.MedianValues) != 0 + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesResponse does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesResponse) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.GetAllMedianValuesResponse.median_values": + x.MedianValues = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesResponse does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_GetAllMedianValuesResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.GetAllMedianValuesResponse.median_values": + if len(x.MedianValues) == 0 { + return protoreflect.ValueOfList(&_GetAllMedianValuesResponse_1_list{}) + } + listValue := &_GetAllMedianValuesResponse_1_list{list: &x.MedianValues} + return protoreflect.ValueOfList(listValue) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesResponse does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.GetAllMedianValuesResponse.median_values": + lv := value.List() + clv := lv.(*_GetAllMedianValuesResponse_1_list) + x.MedianValues = *clv.list + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesResponse does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.GetAllMedianValuesResponse.median_values": + if x.MedianValues == nil { + x.MedianValues = []*MedianValues{} + } + value := &_GetAllMedianValuesResponse_1_list{list: &x.MedianValues} + return protoreflect.ValueOfList(value) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesResponse does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_GetAllMedianValuesResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.GetAllMedianValuesResponse.median_values": + list := []*MedianValues{} + return protoreflect.ValueOfList(&_GetAllMedianValuesResponse_1_list{list: &list}) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetAllMedianValuesResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetAllMedianValuesResponse does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_GetAllMedianValuesResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.GetAllMedianValuesResponse", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_GetAllMedianValuesResponse) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetAllMedianValuesResponse) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_GetAllMedianValuesResponse) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_GetAllMedianValuesResponse) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*GetAllMedianValuesResponse) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if len(x.MedianValues) > 0 { + for _, e := range x.MedianValues { + l = options.Size(e) + n += 1 + l + runtime.Sov(uint64(l)) + } + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*GetAllMedianValuesResponse) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.MedianValues) > 0 { + for iNdEx := len(x.MedianValues) - 1; iNdEx >= 0; iNdEx-- { + encoded, err := options.Marshal(x.MedianValues[iNdEx]) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0xa + } + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*GetAllMedianValuesResponse) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetAllMedianValuesResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetAllMedianValuesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MedianValues", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.MedianValues = append(x.MedianValues, &MedianValues{}) + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.MedianValues[len(x.MedianValues)-1]); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_GetMedianValueRequest protoreflect.MessageDescriptor + fd_GetMedianValueRequest_query_data protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_median_values_proto_init() + md_GetMedianValueRequest = File_layer_daemons_median_values_proto.Messages().ByName("GetMedianValueRequest") + fd_GetMedianValueRequest_query_data = md_GetMedianValueRequest.Fields().ByName("query_data") +} + +var _ protoreflect.Message = (*fastReflection_GetMedianValueRequest)(nil) + +type fastReflection_GetMedianValueRequest GetMedianValueRequest + +func (x *GetMedianValueRequest) ProtoReflect() protoreflect.Message { + return (*fastReflection_GetMedianValueRequest)(x) +} + +func (x *GetMedianValueRequest) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_median_values_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_GetMedianValueRequest_messageType fastReflection_GetMedianValueRequest_messageType +var _ protoreflect.MessageType = fastReflection_GetMedianValueRequest_messageType{} + +type fastReflection_GetMedianValueRequest_messageType struct{} + +func (x fastReflection_GetMedianValueRequest_messageType) Zero() protoreflect.Message { + return (*fastReflection_GetMedianValueRequest)(nil) +} +func (x fastReflection_GetMedianValueRequest_messageType) New() protoreflect.Message { + return new(fastReflection_GetMedianValueRequest) +} +func (x fastReflection_GetMedianValueRequest_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_GetMedianValueRequest +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_GetMedianValueRequest) Descriptor() protoreflect.MessageDescriptor { + return md_GetMedianValueRequest +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_GetMedianValueRequest) Type() protoreflect.MessageType { + return _fastReflection_GetMedianValueRequest_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_GetMedianValueRequest) New() protoreflect.Message { + return new(fastReflection_GetMedianValueRequest) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_GetMedianValueRequest) Interface() protoreflect.ProtoMessage { + return (*GetMedianValueRequest)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_GetMedianValueRequest) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.QueryData != "" { + value := protoreflect.ValueOfString(x.QueryData) + if !f(fd_GetMedianValueRequest_query_data, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_GetMedianValueRequest) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.GetMedianValueRequest.query_data": + return x.QueryData != "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueRequest does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueRequest) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.GetMedianValueRequest.query_data": + x.QueryData = "" + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueRequest does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_GetMedianValueRequest) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.GetMedianValueRequest.query_data": + value := x.QueryData + return protoreflect.ValueOfString(value) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueRequest does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueRequest) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.GetMedianValueRequest.query_data": + x.QueryData = value.Interface().(string) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueRequest does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueRequest) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.GetMedianValueRequest.query_data": + panic(fmt.Errorf("field query_data of message layer.daemons.GetMedianValueRequest is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueRequest does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_GetMedianValueRequest) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.GetMedianValueRequest.query_data": + return protoreflect.ValueOfString("") + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueRequest")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueRequest does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_GetMedianValueRequest) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.GetMedianValueRequest", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_GetMedianValueRequest) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueRequest) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_GetMedianValueRequest) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_GetMedianValueRequest) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*GetMedianValueRequest) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + l = len(x.QueryData) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*GetMedianValueRequest) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.QueryData) > 0 { + i -= len(x.QueryData) + copy(dAtA[i:], x.QueryData) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.QueryData))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*GetMedianValueRequest) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetMedianValueRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetMedianValueRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field QueryData", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.QueryData = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_GetMedianValueResponse protoreflect.MessageDescriptor + fd_GetMedianValueResponse_median_values protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_median_values_proto_init() + md_GetMedianValueResponse = File_layer_daemons_median_values_proto.Messages().ByName("GetMedianValueResponse") + fd_GetMedianValueResponse_median_values = md_GetMedianValueResponse.Fields().ByName("median_values") +} + +var _ protoreflect.Message = (*fastReflection_GetMedianValueResponse)(nil) + +type fastReflection_GetMedianValueResponse GetMedianValueResponse + +func (x *GetMedianValueResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_GetMedianValueResponse)(x) +} + +func (x *GetMedianValueResponse) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_median_values_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_GetMedianValueResponse_messageType fastReflection_GetMedianValueResponse_messageType +var _ protoreflect.MessageType = fastReflection_GetMedianValueResponse_messageType{} + +type fastReflection_GetMedianValueResponse_messageType struct{} + +func (x fastReflection_GetMedianValueResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_GetMedianValueResponse)(nil) +} +func (x fastReflection_GetMedianValueResponse_messageType) New() protoreflect.Message { + return new(fastReflection_GetMedianValueResponse) +} +func (x fastReflection_GetMedianValueResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_GetMedianValueResponse +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_GetMedianValueResponse) Descriptor() protoreflect.MessageDescriptor { + return md_GetMedianValueResponse +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_GetMedianValueResponse) Type() protoreflect.MessageType { + return _fastReflection_GetMedianValueResponse_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_GetMedianValueResponse) New() protoreflect.Message { + return new(fastReflection_GetMedianValueResponse) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_GetMedianValueResponse) Interface() protoreflect.ProtoMessage { + return (*GetMedianValueResponse)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_GetMedianValueResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.MedianValues != nil { + value := protoreflect.ValueOfMessage(x.MedianValues.ProtoReflect()) + if !f(fd_GetMedianValueResponse_median_values, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_GetMedianValueResponse) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.GetMedianValueResponse.median_values": + return x.MedianValues != nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueResponse does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueResponse) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.GetMedianValueResponse.median_values": + x.MedianValues = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueResponse does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_GetMedianValueResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.GetMedianValueResponse.median_values": + value := x.MedianValues + return protoreflect.ValueOfMessage(value.ProtoReflect()) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueResponse does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.GetMedianValueResponse.median_values": + x.MedianValues = value.Message().Interface().(*MedianValues) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueResponse does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.GetMedianValueResponse.median_values": + if x.MedianValues == nil { + x.MedianValues = new(MedianValues) + } + return protoreflect.ValueOfMessage(x.MedianValues.ProtoReflect()) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueResponse does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_GetMedianValueResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.GetMedianValueResponse.median_values": + m := new(MedianValues) + return protoreflect.ValueOfMessage(m.ProtoReflect()) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.GetMedianValueResponse")) + } + panic(fmt.Errorf("message layer.daemons.GetMedianValueResponse does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_GetMedianValueResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.GetMedianValueResponse", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_GetMedianValueResponse) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_GetMedianValueResponse) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_GetMedianValueResponse) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_GetMedianValueResponse) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*GetMedianValueResponse) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if x.MedianValues != nil { + l = options.Size(x.MedianValues) + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*GetMedianValueResponse) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if x.MedianValues != nil { + encoded, err := options.Marshal(x.MedianValues) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*GetMedianValueResponse) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetMedianValueResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: GetMedianValueResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MedianValues", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if x.MedianValues == nil { + x.MedianValues = &MedianValues{} + } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.MedianValues); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.0 +// protoc (unknown) +// source: layer/daemons/median_values.proto + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// MedianValues is the median value for a market +type MedianValues struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // marketId is the market id for a pair + MarketId uint32 `protobuf:"varint,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + Price uint64 `protobuf:"varint,2,opt,name=price,proto3" json:"price,omitempty"` + Exponent int32 `protobuf:"varint,3,opt,name=exponent,proto3" json:"exponent,omitempty"` +} + +func (x *MedianValues) Reset() { + *x = MedianValues{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_median_values_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MedianValues) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MedianValues) ProtoMessage() {} + +// Deprecated: Use MedianValues.ProtoReflect.Descriptor instead. +func (*MedianValues) Descriptor() ([]byte, []int) { + return file_layer_daemons_median_values_proto_rawDescGZIP(), []int{0} +} + +func (x *MedianValues) GetMarketId() uint32 { + if x != nil { + return x.MarketId + } + return 0 +} + +func (x *MedianValues) GetPrice() uint64 { + if x != nil { + return x.Price + } + return 0 +} + +func (x *MedianValues) GetExponent() int32 { + if x != nil { + return x.Exponent + } + return 0 +} + +// GetAllMedianValuesRequest is the request for the GetAllMedianValues rpc +type GetAllMedianValuesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *GetAllMedianValuesRequest) Reset() { + *x = GetAllMedianValuesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_median_values_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetAllMedianValuesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllMedianValuesRequest) ProtoMessage() {} + +// Deprecated: Use GetAllMedianValuesRequest.ProtoReflect.Descriptor instead. +func (*GetAllMedianValuesRequest) Descriptor() ([]byte, []int) { + return file_layer_daemons_median_values_proto_rawDescGZIP(), []int{1} +} + +// GetAllMedianValuesResponse is the response for the GetAllMedianValues rpc +type GetAllMedianValuesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MedianValues []*MedianValues `protobuf:"bytes,1,rep,name=median_values,json=medianValues,proto3" json:"median_values,omitempty"` +} + +func (x *GetAllMedianValuesResponse) Reset() { + *x = GetAllMedianValuesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_median_values_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetAllMedianValuesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAllMedianValuesResponse) ProtoMessage() {} + +// Deprecated: Use GetAllMedianValuesResponse.ProtoReflect.Descriptor instead. +func (*GetAllMedianValuesResponse) Descriptor() ([]byte, []int) { + return file_layer_daemons_median_values_proto_rawDescGZIP(), []int{2} +} + +func (x *GetAllMedianValuesResponse) GetMedianValues() []*MedianValues { + if x != nil { + return x.MedianValues + } + return nil +} + +// GetMedianValuesRequest is the request for the GetMedianValues rpc +type GetMedianValueRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // query data to fetch prices for + QueryData string `protobuf:"bytes,1,opt,name=query_data,json=queryData,proto3" json:"query_data,omitempty"` +} + +func (x *GetMedianValueRequest) Reset() { + *x = GetMedianValueRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_median_values_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetMedianValueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMedianValueRequest) ProtoMessage() {} + +// Deprecated: Use GetMedianValueRequest.ProtoReflect.Descriptor instead. +func (*GetMedianValueRequest) Descriptor() ([]byte, []int) { + return file_layer_daemons_median_values_proto_rawDescGZIP(), []int{3} +} + +func (x *GetMedianValueRequest) GetQueryData() string { + if x != nil { + return x.QueryData + } + return "" +} + +// GetMedianValueResponse is the response for the GetMedianValue rpc +type GetMedianValueResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MedianValues *MedianValues `protobuf:"bytes,1,opt,name=median_values,json=medianValues,proto3" json:"median_values,omitempty"` +} + +func (x *GetMedianValueResponse) Reset() { + *x = GetMedianValueResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_median_values_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetMedianValueResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMedianValueResponse) ProtoMessage() {} + +// Deprecated: Use GetMedianValueResponse.ProtoReflect.Descriptor instead. +func (*GetMedianValueResponse) Descriptor() ([]byte, []int) { + return file_layer_daemons_median_values_proto_rawDescGZIP(), []int{4} +} + +func (x *GetMedianValueResponse) GetMedianValues() *MedianValues { + if x != nil { + return x.MedianValues + } + return nil +} + +var File_layer_daemons_median_values_proto protoreflect.FileDescriptor + +var file_layer_daemons_median_values_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2f, + 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x73, 0x1a, 0x14, 0x67, 0x6f, 0x67, 0x6f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, + 0x67, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5d, 0x0a, 0x0c, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x61, 0x72, 0x6b, 0x65, + 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x65, 0x78, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x22, 0x1b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x4d, + 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x5e, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x4d, 0x65, 0x64, 0x69, + 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x40, 0x0a, 0x0d, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x52, 0x0c, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x22, 0x36, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x71, + 0x75, 0x65, 0x72, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x71, 0x75, 0x65, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x22, 0x5a, 0x0a, 0x16, 0x47, 0x65, + 0x74, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0d, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x61, + 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x65, 0x64, 0x69, + 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x0c, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x32, 0xbd, 0x02, 0x0a, 0x13, 0x4d, 0x65, 0x64, 0x69, 0x61, + 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x93, + 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x28, 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x4d, 0x65, 0x64, 0x69, + 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x29, 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, + 0x47, 0x65, 0x74, 0x41, 0x6c, 0x6c, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x22, 0x12, 0x20, 0x2f, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, + 0x65, 0x74, 0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x64, 0x69, + 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x24, 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x64, 0x69, 0x61, + 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, + 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x47, 0x65, + 0x74, 0x4d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2a, 0x12, 0x28, 0x2f, 0x6c, + 0x61, 0x79, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x65, 0x64, + 0x69, 0x61, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2f, 0x7b, 0x71, 0x75, 0x65, 0x72, 0x79, + 0x5f, 0x64, 0x61, 0x74, 0x61, 0x7d, 0x42, 0x9b, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x6c, + 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x42, 0x11, 0x4d, 0x65, + 0x64, 0x69, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x1e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x73, 0xa2, 0x02, 0x03, 0x4c, 0x44, 0x58, 0xaa, 0x02, 0x0d, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x2e, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0xca, 0x02, 0x0d, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x5c, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0xe2, 0x02, 0x19, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x5c, + 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x3a, 0x3a, 0x44, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_layer_daemons_median_values_proto_rawDescOnce sync.Once + file_layer_daemons_median_values_proto_rawDescData = file_layer_daemons_median_values_proto_rawDesc +) + +func file_layer_daemons_median_values_proto_rawDescGZIP() []byte { + file_layer_daemons_median_values_proto_rawDescOnce.Do(func() { + file_layer_daemons_median_values_proto_rawDescData = protoimpl.X.CompressGZIP(file_layer_daemons_median_values_proto_rawDescData) + }) + return file_layer_daemons_median_values_proto_rawDescData +} + +var file_layer_daemons_median_values_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_layer_daemons_median_values_proto_goTypes = []interface{}{ + (*MedianValues)(nil), // 0: layer.daemons.MedianValues + (*GetAllMedianValuesRequest)(nil), // 1: layer.daemons.GetAllMedianValuesRequest + (*GetAllMedianValuesResponse)(nil), // 2: layer.daemons.GetAllMedianValuesResponse + (*GetMedianValueRequest)(nil), // 3: layer.daemons.GetMedianValueRequest + (*GetMedianValueResponse)(nil), // 4: layer.daemons.GetMedianValueResponse +} +var file_layer_daemons_median_values_proto_depIdxs = []int32{ + 0, // 0: layer.daemons.GetAllMedianValuesResponse.median_values:type_name -> layer.daemons.MedianValues + 0, // 1: layer.daemons.GetMedianValueResponse.median_values:type_name -> layer.daemons.MedianValues + 1, // 2: layer.daemons.MedianValuesService.GetAllMedianValues:input_type -> layer.daemons.GetAllMedianValuesRequest + 3, // 3: layer.daemons.MedianValuesService.GetMedianValue:input_type -> layer.daemons.GetMedianValueRequest + 2, // 4: layer.daemons.MedianValuesService.GetAllMedianValues:output_type -> layer.daemons.GetAllMedianValuesResponse + 4, // 5: layer.daemons.MedianValuesService.GetMedianValue:output_type -> layer.daemons.GetMedianValueResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_layer_daemons_median_values_proto_init() } +func file_layer_daemons_median_values_proto_init() { + if File_layer_daemons_median_values_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_layer_daemons_median_values_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MedianValues); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_median_values_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetAllMedianValuesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_median_values_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetAllMedianValuesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_median_values_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetMedianValueRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_median_values_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetMedianValueResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_layer_daemons_median_values_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_layer_daemons_median_values_proto_goTypes, + DependencyIndexes: file_layer_daemons_median_values_proto_depIdxs, + MessageInfos: file_layer_daemons_median_values_proto_msgTypes, + }.Build() + File_layer_daemons_median_values_proto = out.File + file_layer_daemons_median_values_proto_rawDesc = nil + file_layer_daemons_median_values_proto_goTypes = nil + file_layer_daemons_median_values_proto_depIdxs = nil +} diff --git a/api/layer/daemons/median_values_grpc.pb.go b/api/layer/daemons/median_values_grpc.pb.go new file mode 100644 index 000000000..0f5c8708e --- /dev/null +++ b/api/layer/daemons/median_values_grpc.pb.go @@ -0,0 +1,141 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package daemons + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// MedianValuesServiceClient is the client API for MedianValuesService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type MedianValuesServiceClient interface { + // GetAllMedianValues returns the median values for all markets + GetAllMedianValues(ctx context.Context, in *GetAllMedianValuesRequest, opts ...grpc.CallOption) (*GetAllMedianValuesResponse, error) + // GetMedianValue returns the median value for a market given a query data that a market is associated with + GetMedianValue(ctx context.Context, in *GetMedianValueRequest, opts ...grpc.CallOption) (*GetMedianValueResponse, error) +} + +type medianValuesServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewMedianValuesServiceClient(cc grpc.ClientConnInterface) MedianValuesServiceClient { + return &medianValuesServiceClient{cc} +} + +func (c *medianValuesServiceClient) GetAllMedianValues(ctx context.Context, in *GetAllMedianValuesRequest, opts ...grpc.CallOption) (*GetAllMedianValuesResponse, error) { + out := new(GetAllMedianValuesResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.MedianValuesService/GetAllMedianValues", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *medianValuesServiceClient) GetMedianValue(ctx context.Context, in *GetMedianValueRequest, opts ...grpc.CallOption) (*GetMedianValueResponse, error) { + out := new(GetMedianValueResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.MedianValuesService/GetMedianValue", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MedianValuesServiceServer is the server API for MedianValuesService service. +// All implementations must embed UnimplementedMedianValuesServiceServer +// for forward compatibility +type MedianValuesServiceServer interface { + // GetAllMedianValues returns the median values for all markets + GetAllMedianValues(context.Context, *GetAllMedianValuesRequest) (*GetAllMedianValuesResponse, error) + // GetMedianValue returns the median value for a market given a query data that a market is associated with + GetMedianValue(context.Context, *GetMedianValueRequest) (*GetMedianValueResponse, error) + mustEmbedUnimplementedMedianValuesServiceServer() +} + +// UnimplementedMedianValuesServiceServer must be embedded to have forward compatible implementations. +type UnimplementedMedianValuesServiceServer struct { +} + +func (UnimplementedMedianValuesServiceServer) GetAllMedianValues(context.Context, *GetAllMedianValuesRequest) (*GetAllMedianValuesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAllMedianValues not implemented") +} +func (UnimplementedMedianValuesServiceServer) GetMedianValue(context.Context, *GetMedianValueRequest) (*GetMedianValueResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMedianValue not implemented") +} +func (UnimplementedMedianValuesServiceServer) mustEmbedUnimplementedMedianValuesServiceServer() {} + +// UnsafeMedianValuesServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to MedianValuesServiceServer will +// result in compilation errors. +type UnsafeMedianValuesServiceServer interface { + mustEmbedUnimplementedMedianValuesServiceServer() +} + +func RegisterMedianValuesServiceServer(s grpc.ServiceRegistrar, srv MedianValuesServiceServer) { + s.RegisterService(&MedianValuesService_ServiceDesc, srv) +} + +func _MedianValuesService_GetAllMedianValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAllMedianValuesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MedianValuesServiceServer).GetAllMedianValues(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.daemons.MedianValuesService/GetAllMedianValues", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MedianValuesServiceServer).GetAllMedianValues(ctx, req.(*GetAllMedianValuesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MedianValuesService_GetMedianValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMedianValueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MedianValuesServiceServer).GetMedianValue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.daemons.MedianValuesService/GetMedianValue", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MedianValuesServiceServer).GetMedianValue(ctx, req.(*GetMedianValueRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// MedianValuesService_ServiceDesc is the grpc.ServiceDesc for MedianValuesService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var MedianValuesService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "layer.daemons.MedianValuesService", + HandlerType: (*MedianValuesServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetAllMedianValues", + Handler: _MedianValuesService_GetAllMedianValues_Handler, + }, + { + MethodName: "GetMedianValue", + Handler: _MedianValuesService_GetMedianValue_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "layer/daemons/median_values.proto", +} diff --git a/api/layer/daemons/pricefeed.pulsar.go b/api/layer/daemons/pricefeed.pulsar.go new file mode 100644 index 000000000..419aabf67 --- /dev/null +++ b/api/layer/daemons/pricefeed.pulsar.go @@ -0,0 +1,2293 @@ +// Code generated by protoc-gen-go-pulsar. DO NOT EDIT. +package daemons + +import ( + fmt "fmt" + runtime "github.com/cosmos/cosmos-proto/runtime" + _ "github.com/cosmos/gogoproto/gogoproto" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoiface "google.golang.org/protobuf/runtime/protoiface" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + io "io" + reflect "reflect" + sync "sync" +) + +var _ protoreflect.List = (*_UpdateMarketPricesRequest_1_list)(nil) + +type _UpdateMarketPricesRequest_1_list struct { + list *[]*MarketPriceUpdate +} + +func (x *_UpdateMarketPricesRequest_1_list) Len() int { + if x.list == nil { + return 0 + } + return len(*x.list) +} + +func (x *_UpdateMarketPricesRequest_1_list) Get(i int) protoreflect.Value { + return protoreflect.ValueOfMessage((*x.list)[i].ProtoReflect()) +} + +func (x *_UpdateMarketPricesRequest_1_list) Set(i int, value protoreflect.Value) { + valueUnwrapped := value.Message() + concreteValue := valueUnwrapped.Interface().(*MarketPriceUpdate) + (*x.list)[i] = concreteValue +} + +func (x *_UpdateMarketPricesRequest_1_list) Append(value protoreflect.Value) { + valueUnwrapped := value.Message() + concreteValue := valueUnwrapped.Interface().(*MarketPriceUpdate) + *x.list = append(*x.list, concreteValue) +} + +func (x *_UpdateMarketPricesRequest_1_list) AppendMutable() protoreflect.Value { + v := new(MarketPriceUpdate) + *x.list = append(*x.list, v) + return protoreflect.ValueOfMessage(v.ProtoReflect()) +} + +func (x *_UpdateMarketPricesRequest_1_list) Truncate(n int) { + for i := n; i < len(*x.list); i++ { + (*x.list)[i] = nil + } + *x.list = (*x.list)[:n] +} + +func (x *_UpdateMarketPricesRequest_1_list) NewElement() protoreflect.Value { + v := new(MarketPriceUpdate) + return protoreflect.ValueOfMessage(v.ProtoReflect()) +} + +func (x *_UpdateMarketPricesRequest_1_list) IsValid() bool { + return x.list != nil +} + +var ( + md_UpdateMarketPricesRequest protoreflect.MessageDescriptor + fd_UpdateMarketPricesRequest_market_price_updates protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_pricefeed_proto_init() + md_UpdateMarketPricesRequest = File_layer_daemons_pricefeed_proto.Messages().ByName("UpdateMarketPricesRequest") + fd_UpdateMarketPricesRequest_market_price_updates = md_UpdateMarketPricesRequest.Fields().ByName("market_price_updates") +} + +var _ protoreflect.Message = (*fastReflection_UpdateMarketPricesRequest)(nil) + +type fastReflection_UpdateMarketPricesRequest UpdateMarketPricesRequest + +func (x *UpdateMarketPricesRequest) ProtoReflect() protoreflect.Message { + return (*fastReflection_UpdateMarketPricesRequest)(x) +} + +func (x *UpdateMarketPricesRequest) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_UpdateMarketPricesRequest_messageType fastReflection_UpdateMarketPricesRequest_messageType +var _ protoreflect.MessageType = fastReflection_UpdateMarketPricesRequest_messageType{} + +type fastReflection_UpdateMarketPricesRequest_messageType struct{} + +func (x fastReflection_UpdateMarketPricesRequest_messageType) Zero() protoreflect.Message { + return (*fastReflection_UpdateMarketPricesRequest)(nil) +} +func (x fastReflection_UpdateMarketPricesRequest_messageType) New() protoreflect.Message { + return new(fastReflection_UpdateMarketPricesRequest) +} +func (x fastReflection_UpdateMarketPricesRequest_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_UpdateMarketPricesRequest +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_UpdateMarketPricesRequest) Descriptor() protoreflect.MessageDescriptor { + return md_UpdateMarketPricesRequest +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_UpdateMarketPricesRequest) Type() protoreflect.MessageType { + return _fastReflection_UpdateMarketPricesRequest_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_UpdateMarketPricesRequest) New() protoreflect.Message { + return new(fastReflection_UpdateMarketPricesRequest) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_UpdateMarketPricesRequest) Interface() protoreflect.ProtoMessage { + return (*UpdateMarketPricesRequest)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_UpdateMarketPricesRequest) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if len(x.MarketPriceUpdates) != 0 { + value := protoreflect.ValueOfList(&_UpdateMarketPricesRequest_1_list{list: &x.MarketPriceUpdates}) + if !f(fd_UpdateMarketPricesRequest_market_price_updates, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_UpdateMarketPricesRequest) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.UpdateMarketPricesRequest.market_price_updates": + return len(x.MarketPriceUpdates) != 0 + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesRequest")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesRequest does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesRequest) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.UpdateMarketPricesRequest.market_price_updates": + x.MarketPriceUpdates = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesRequest")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesRequest does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_UpdateMarketPricesRequest) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.UpdateMarketPricesRequest.market_price_updates": + if len(x.MarketPriceUpdates) == 0 { + return protoreflect.ValueOfList(&_UpdateMarketPricesRequest_1_list{}) + } + listValue := &_UpdateMarketPricesRequest_1_list{list: &x.MarketPriceUpdates} + return protoreflect.ValueOfList(listValue) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesRequest")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesRequest does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesRequest) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.UpdateMarketPricesRequest.market_price_updates": + lv := value.List() + clv := lv.(*_UpdateMarketPricesRequest_1_list) + x.MarketPriceUpdates = *clv.list + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesRequest")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesRequest does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesRequest) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.UpdateMarketPricesRequest.market_price_updates": + if x.MarketPriceUpdates == nil { + x.MarketPriceUpdates = []*MarketPriceUpdate{} + } + value := &_UpdateMarketPricesRequest_1_list{list: &x.MarketPriceUpdates} + return protoreflect.ValueOfList(value) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesRequest")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesRequest does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_UpdateMarketPricesRequest) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.UpdateMarketPricesRequest.market_price_updates": + list := []*MarketPriceUpdate{} + return protoreflect.ValueOfList(&_UpdateMarketPricesRequest_1_list{list: &list}) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesRequest")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesRequest does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_UpdateMarketPricesRequest) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.UpdateMarketPricesRequest", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_UpdateMarketPricesRequest) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesRequest) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_UpdateMarketPricesRequest) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_UpdateMarketPricesRequest) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*UpdateMarketPricesRequest) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if len(x.MarketPriceUpdates) > 0 { + for _, e := range x.MarketPriceUpdates { + l = options.Size(e) + n += 1 + l + runtime.Sov(uint64(l)) + } + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*UpdateMarketPricesRequest) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.MarketPriceUpdates) > 0 { + for iNdEx := len(x.MarketPriceUpdates) - 1; iNdEx >= 0; iNdEx-- { + encoded, err := options.Marshal(x.MarketPriceUpdates[iNdEx]) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0xa + } + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*UpdateMarketPricesRequest) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: UpdateMarketPricesRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: UpdateMarketPricesRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MarketPriceUpdates", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.MarketPriceUpdates = append(x.MarketPriceUpdates, &MarketPriceUpdate{}) + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.MarketPriceUpdates[len(x.MarketPriceUpdates)-1]); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_UpdateMarketPricesResponse protoreflect.MessageDescriptor +) + +func init() { + file_layer_daemons_pricefeed_proto_init() + md_UpdateMarketPricesResponse = File_layer_daemons_pricefeed_proto.Messages().ByName("UpdateMarketPricesResponse") +} + +var _ protoreflect.Message = (*fastReflection_UpdateMarketPricesResponse)(nil) + +type fastReflection_UpdateMarketPricesResponse UpdateMarketPricesResponse + +func (x *UpdateMarketPricesResponse) ProtoReflect() protoreflect.Message { + return (*fastReflection_UpdateMarketPricesResponse)(x) +} + +func (x *UpdateMarketPricesResponse) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_UpdateMarketPricesResponse_messageType fastReflection_UpdateMarketPricesResponse_messageType +var _ protoreflect.MessageType = fastReflection_UpdateMarketPricesResponse_messageType{} + +type fastReflection_UpdateMarketPricesResponse_messageType struct{} + +func (x fastReflection_UpdateMarketPricesResponse_messageType) Zero() protoreflect.Message { + return (*fastReflection_UpdateMarketPricesResponse)(nil) +} +func (x fastReflection_UpdateMarketPricesResponse_messageType) New() protoreflect.Message { + return new(fastReflection_UpdateMarketPricesResponse) +} +func (x fastReflection_UpdateMarketPricesResponse_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_UpdateMarketPricesResponse +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_UpdateMarketPricesResponse) Descriptor() protoreflect.MessageDescriptor { + return md_UpdateMarketPricesResponse +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_UpdateMarketPricesResponse) Type() protoreflect.MessageType { + return _fastReflection_UpdateMarketPricesResponse_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_UpdateMarketPricesResponse) New() protoreflect.Message { + return new(fastReflection_UpdateMarketPricesResponse) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_UpdateMarketPricesResponse) Interface() protoreflect.ProtoMessage { + return (*UpdateMarketPricesResponse)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_UpdateMarketPricesResponse) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_UpdateMarketPricesResponse) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesResponse")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesResponse does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesResponse) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesResponse")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesResponse does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_UpdateMarketPricesResponse) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesResponse")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesResponse does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesResponse) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesResponse")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesResponse does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesResponse) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesResponse")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesResponse does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_UpdateMarketPricesResponse) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.UpdateMarketPricesResponse")) + } + panic(fmt.Errorf("message layer.daemons.UpdateMarketPricesResponse does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_UpdateMarketPricesResponse) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.UpdateMarketPricesResponse", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_UpdateMarketPricesResponse) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_UpdateMarketPricesResponse) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_UpdateMarketPricesResponse) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_UpdateMarketPricesResponse) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*UpdateMarketPricesResponse) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*UpdateMarketPricesResponse) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*UpdateMarketPricesResponse) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: UpdateMarketPricesResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: UpdateMarketPricesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var ( + md_ExchangePrice protoreflect.MessageDescriptor + fd_ExchangePrice_exchange_id protoreflect.FieldDescriptor + fd_ExchangePrice_price protoreflect.FieldDescriptor + fd_ExchangePrice_last_update_time protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_pricefeed_proto_init() + md_ExchangePrice = File_layer_daemons_pricefeed_proto.Messages().ByName("ExchangePrice") + fd_ExchangePrice_exchange_id = md_ExchangePrice.Fields().ByName("exchange_id") + fd_ExchangePrice_price = md_ExchangePrice.Fields().ByName("price") + fd_ExchangePrice_last_update_time = md_ExchangePrice.Fields().ByName("last_update_time") +} + +var _ protoreflect.Message = (*fastReflection_ExchangePrice)(nil) + +type fastReflection_ExchangePrice ExchangePrice + +func (x *ExchangePrice) ProtoReflect() protoreflect.Message { + return (*fastReflection_ExchangePrice)(x) +} + +func (x *ExchangePrice) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_ExchangePrice_messageType fastReflection_ExchangePrice_messageType +var _ protoreflect.MessageType = fastReflection_ExchangePrice_messageType{} + +type fastReflection_ExchangePrice_messageType struct{} + +func (x fastReflection_ExchangePrice_messageType) Zero() protoreflect.Message { + return (*fastReflection_ExchangePrice)(nil) +} +func (x fastReflection_ExchangePrice_messageType) New() protoreflect.Message { + return new(fastReflection_ExchangePrice) +} +func (x fastReflection_ExchangePrice_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_ExchangePrice +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_ExchangePrice) Descriptor() protoreflect.MessageDescriptor { + return md_ExchangePrice +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_ExchangePrice) Type() protoreflect.MessageType { + return _fastReflection_ExchangePrice_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_ExchangePrice) New() protoreflect.Message { + return new(fastReflection_ExchangePrice) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_ExchangePrice) Interface() protoreflect.ProtoMessage { + return (*ExchangePrice)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_ExchangePrice) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.ExchangeId != "" { + value := protoreflect.ValueOfString(x.ExchangeId) + if !f(fd_ExchangePrice_exchange_id, value) { + return + } + } + if x.Price != uint64(0) { + value := protoreflect.ValueOfUint64(x.Price) + if !f(fd_ExchangePrice_price, value) { + return + } + } + if x.LastUpdateTime != nil { + value := protoreflect.ValueOfMessage(x.LastUpdateTime.ProtoReflect()) + if !f(fd_ExchangePrice_last_update_time, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_ExchangePrice) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.ExchangePrice.exchange_id": + return x.ExchangeId != "" + case "layer.daemons.ExchangePrice.price": + return x.Price != uint64(0) + case "layer.daemons.ExchangePrice.last_update_time": + return x.LastUpdateTime != nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.ExchangePrice")) + } + panic(fmt.Errorf("message layer.daemons.ExchangePrice does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_ExchangePrice) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.ExchangePrice.exchange_id": + x.ExchangeId = "" + case "layer.daemons.ExchangePrice.price": + x.Price = uint64(0) + case "layer.daemons.ExchangePrice.last_update_time": + x.LastUpdateTime = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.ExchangePrice")) + } + panic(fmt.Errorf("message layer.daemons.ExchangePrice does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_ExchangePrice) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.ExchangePrice.exchange_id": + value := x.ExchangeId + return protoreflect.ValueOfString(value) + case "layer.daemons.ExchangePrice.price": + value := x.Price + return protoreflect.ValueOfUint64(value) + case "layer.daemons.ExchangePrice.last_update_time": + value := x.LastUpdateTime + return protoreflect.ValueOfMessage(value.ProtoReflect()) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.ExchangePrice")) + } + panic(fmt.Errorf("message layer.daemons.ExchangePrice does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_ExchangePrice) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.ExchangePrice.exchange_id": + x.ExchangeId = value.Interface().(string) + case "layer.daemons.ExchangePrice.price": + x.Price = value.Uint() + case "layer.daemons.ExchangePrice.last_update_time": + x.LastUpdateTime = value.Message().Interface().(*timestamppb.Timestamp) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.ExchangePrice")) + } + panic(fmt.Errorf("message layer.daemons.ExchangePrice does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_ExchangePrice) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.ExchangePrice.last_update_time": + if x.LastUpdateTime == nil { + x.LastUpdateTime = new(timestamppb.Timestamp) + } + return protoreflect.ValueOfMessage(x.LastUpdateTime.ProtoReflect()) + case "layer.daemons.ExchangePrice.exchange_id": + panic(fmt.Errorf("field exchange_id of message layer.daemons.ExchangePrice is not mutable")) + case "layer.daemons.ExchangePrice.price": + panic(fmt.Errorf("field price of message layer.daemons.ExchangePrice is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.ExchangePrice")) + } + panic(fmt.Errorf("message layer.daemons.ExchangePrice does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_ExchangePrice) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.ExchangePrice.exchange_id": + return protoreflect.ValueOfString("") + case "layer.daemons.ExchangePrice.price": + return protoreflect.ValueOfUint64(uint64(0)) + case "layer.daemons.ExchangePrice.last_update_time": + m := new(timestamppb.Timestamp) + return protoreflect.ValueOfMessage(m.ProtoReflect()) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.ExchangePrice")) + } + panic(fmt.Errorf("message layer.daemons.ExchangePrice does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_ExchangePrice) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.ExchangePrice", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_ExchangePrice) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_ExchangePrice) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_ExchangePrice) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_ExchangePrice) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*ExchangePrice) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + l = len(x.ExchangeId) + if l > 0 { + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.Price != 0 { + n += 1 + runtime.Sov(uint64(x.Price)) + } + if x.LastUpdateTime != nil { + l = options.Size(x.LastUpdateTime) + n += 1 + l + runtime.Sov(uint64(l)) + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*ExchangePrice) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if x.LastUpdateTime != nil { + encoded, err := options.Marshal(x.LastUpdateTime) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0x1a + } + if x.Price != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.Price)) + i-- + dAtA[i] = 0x10 + } + if len(x.ExchangeId) > 0 { + i -= len(x.ExchangeId) + copy(dAtA[i:], x.ExchangeId) + i = runtime.EncodeVarint(dAtA, i, uint64(len(x.ExchangeId))) + i-- + dAtA[i] = 0xa + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*ExchangePrice) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: ExchangePrice: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: ExchangePrice: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ExchangeId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.ExchangeId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Price", wireType) + } + x.Price = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.Price |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field LastUpdateTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if x.LastUpdateTime == nil { + x.LastUpdateTime = ×tamppb.Timestamp{} + } + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.LastUpdateTime); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +var _ protoreflect.List = (*_MarketPriceUpdate_2_list)(nil) + +type _MarketPriceUpdate_2_list struct { + list *[]*ExchangePrice +} + +func (x *_MarketPriceUpdate_2_list) Len() int { + if x.list == nil { + return 0 + } + return len(*x.list) +} + +func (x *_MarketPriceUpdate_2_list) Get(i int) protoreflect.Value { + return protoreflect.ValueOfMessage((*x.list)[i].ProtoReflect()) +} + +func (x *_MarketPriceUpdate_2_list) Set(i int, value protoreflect.Value) { + valueUnwrapped := value.Message() + concreteValue := valueUnwrapped.Interface().(*ExchangePrice) + (*x.list)[i] = concreteValue +} + +func (x *_MarketPriceUpdate_2_list) Append(value protoreflect.Value) { + valueUnwrapped := value.Message() + concreteValue := valueUnwrapped.Interface().(*ExchangePrice) + *x.list = append(*x.list, concreteValue) +} + +func (x *_MarketPriceUpdate_2_list) AppendMutable() protoreflect.Value { + v := new(ExchangePrice) + *x.list = append(*x.list, v) + return protoreflect.ValueOfMessage(v.ProtoReflect()) +} + +func (x *_MarketPriceUpdate_2_list) Truncate(n int) { + for i := n; i < len(*x.list); i++ { + (*x.list)[i] = nil + } + *x.list = (*x.list)[:n] +} + +func (x *_MarketPriceUpdate_2_list) NewElement() protoreflect.Value { + v := new(ExchangePrice) + return protoreflect.ValueOfMessage(v.ProtoReflect()) +} + +func (x *_MarketPriceUpdate_2_list) IsValid() bool { + return x.list != nil +} + +var ( + md_MarketPriceUpdate protoreflect.MessageDescriptor + fd_MarketPriceUpdate_market_id protoreflect.FieldDescriptor + fd_MarketPriceUpdate_exchange_prices protoreflect.FieldDescriptor +) + +func init() { + file_layer_daemons_pricefeed_proto_init() + md_MarketPriceUpdate = File_layer_daemons_pricefeed_proto.Messages().ByName("MarketPriceUpdate") + fd_MarketPriceUpdate_market_id = md_MarketPriceUpdate.Fields().ByName("market_id") + fd_MarketPriceUpdate_exchange_prices = md_MarketPriceUpdate.Fields().ByName("exchange_prices") +} + +var _ protoreflect.Message = (*fastReflection_MarketPriceUpdate)(nil) + +type fastReflection_MarketPriceUpdate MarketPriceUpdate + +func (x *MarketPriceUpdate) ProtoReflect() protoreflect.Message { + return (*fastReflection_MarketPriceUpdate)(x) +} + +func (x *MarketPriceUpdate) slowProtoReflect() protoreflect.Message { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +var _fastReflection_MarketPriceUpdate_messageType fastReflection_MarketPriceUpdate_messageType +var _ protoreflect.MessageType = fastReflection_MarketPriceUpdate_messageType{} + +type fastReflection_MarketPriceUpdate_messageType struct{} + +func (x fastReflection_MarketPriceUpdate_messageType) Zero() protoreflect.Message { + return (*fastReflection_MarketPriceUpdate)(nil) +} +func (x fastReflection_MarketPriceUpdate_messageType) New() protoreflect.Message { + return new(fastReflection_MarketPriceUpdate) +} +func (x fastReflection_MarketPriceUpdate_messageType) Descriptor() protoreflect.MessageDescriptor { + return md_MarketPriceUpdate +} + +// Descriptor returns message descriptor, which contains only the protobuf +// type information for the message. +func (x *fastReflection_MarketPriceUpdate) Descriptor() protoreflect.MessageDescriptor { + return md_MarketPriceUpdate +} + +// Type returns the message type, which encapsulates both Go and protobuf +// type information. If the Go type information is not needed, +// it is recommended that the message descriptor be used instead. +func (x *fastReflection_MarketPriceUpdate) Type() protoreflect.MessageType { + return _fastReflection_MarketPriceUpdate_messageType +} + +// New returns a newly allocated and mutable empty message. +func (x *fastReflection_MarketPriceUpdate) New() protoreflect.Message { + return new(fastReflection_MarketPriceUpdate) +} + +// Interface unwraps the message reflection interface and +// returns the underlying ProtoMessage interface. +func (x *fastReflection_MarketPriceUpdate) Interface() protoreflect.ProtoMessage { + return (*MarketPriceUpdate)(x) +} + +// Range iterates over every populated field in an undefined order, +// calling f for each field descriptor and value encountered. +// Range returns immediately if f returns false. +// While iterating, mutating operations may only be performed +// on the current field descriptor. +func (x *fastReflection_MarketPriceUpdate) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { + if x.MarketId != uint32(0) { + value := protoreflect.ValueOfUint32(x.MarketId) + if !f(fd_MarketPriceUpdate_market_id, value) { + return + } + } + if len(x.ExchangePrices) != 0 { + value := protoreflect.ValueOfList(&_MarketPriceUpdate_2_list{list: &x.ExchangePrices}) + if !f(fd_MarketPriceUpdate_exchange_prices, value) { + return + } + } +} + +// Has reports whether a field is populated. +// +// Some fields have the property of nullability where it is possible to +// distinguish between the default value of a field and whether the field +// was explicitly populated with the default value. Singular message fields, +// member fields of a oneof, and proto2 scalar fields are nullable. Such +// fields are populated only if explicitly set. +// +// In other cases (aside from the nullable cases above), +// a proto3 scalar field is populated if it contains a non-zero value, and +// a repeated field is populated if it is non-empty. +func (x *fastReflection_MarketPriceUpdate) Has(fd protoreflect.FieldDescriptor) bool { + switch fd.FullName() { + case "layer.daemons.MarketPriceUpdate.market_id": + return x.MarketId != uint32(0) + case "layer.daemons.MarketPriceUpdate.exchange_prices": + return len(x.ExchangePrices) != 0 + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MarketPriceUpdate")) + } + panic(fmt.Errorf("message layer.daemons.MarketPriceUpdate does not contain field %s", fd.FullName())) + } +} + +// Clear clears the field such that a subsequent Has call reports false. +// +// Clearing an extension field clears both the extension type and value +// associated with the given field number. +// +// Clear is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MarketPriceUpdate) Clear(fd protoreflect.FieldDescriptor) { + switch fd.FullName() { + case "layer.daemons.MarketPriceUpdate.market_id": + x.MarketId = uint32(0) + case "layer.daemons.MarketPriceUpdate.exchange_prices": + x.ExchangePrices = nil + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MarketPriceUpdate")) + } + panic(fmt.Errorf("message layer.daemons.MarketPriceUpdate does not contain field %s", fd.FullName())) + } +} + +// Get retrieves the value for a field. +// +// For unpopulated scalars, it returns the default value, where +// the default value of a bytes scalar is guaranteed to be a copy. +// For unpopulated composite types, it returns an empty, read-only view +// of the value; to obtain a mutable reference, use Mutable. +func (x *fastReflection_MarketPriceUpdate) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value { + switch descriptor.FullName() { + case "layer.daemons.MarketPriceUpdate.market_id": + value := x.MarketId + return protoreflect.ValueOfUint32(value) + case "layer.daemons.MarketPriceUpdate.exchange_prices": + if len(x.ExchangePrices) == 0 { + return protoreflect.ValueOfList(&_MarketPriceUpdate_2_list{}) + } + listValue := &_MarketPriceUpdate_2_list{list: &x.ExchangePrices} + return protoreflect.ValueOfList(listValue) + default: + if descriptor.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MarketPriceUpdate")) + } + panic(fmt.Errorf("message layer.daemons.MarketPriceUpdate does not contain field %s", descriptor.FullName())) + } +} + +// Set stores the value for a field. +// +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType. +// When setting a composite type, it is unspecified whether the stored value +// aliases the source's memory in any way. If the composite value is an +// empty, read-only value, then it panics. +// +// Set is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MarketPriceUpdate) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) { + switch fd.FullName() { + case "layer.daemons.MarketPriceUpdate.market_id": + x.MarketId = uint32(value.Uint()) + case "layer.daemons.MarketPriceUpdate.exchange_prices": + lv := value.List() + clv := lv.(*_MarketPriceUpdate_2_list) + x.ExchangePrices = *clv.list + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MarketPriceUpdate")) + } + panic(fmt.Errorf("message layer.daemons.MarketPriceUpdate does not contain field %s", fd.FullName())) + } +} + +// Mutable returns a mutable reference to a composite type. +// +// If the field is unpopulated, it may allocate a composite value. +// For a field belonging to a oneof, it implicitly clears any other field +// that may be currently set within the same oneof. +// For extension fields, it implicitly stores the provided ExtensionType +// if not already stored. +// It panics if the field does not contain a composite type. +// +// Mutable is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MarketPriceUpdate) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.MarketPriceUpdate.exchange_prices": + if x.ExchangePrices == nil { + x.ExchangePrices = []*ExchangePrice{} + } + value := &_MarketPriceUpdate_2_list{list: &x.ExchangePrices} + return protoreflect.ValueOfList(value) + case "layer.daemons.MarketPriceUpdate.market_id": + panic(fmt.Errorf("field market_id of message layer.daemons.MarketPriceUpdate is not mutable")) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MarketPriceUpdate")) + } + panic(fmt.Errorf("message layer.daemons.MarketPriceUpdate does not contain field %s", fd.FullName())) + } +} + +// NewField returns a new value that is assignable to the field +// for the given descriptor. For scalars, this returns the default value. +// For lists, maps, and messages, this returns a new, empty, mutable value. +func (x *fastReflection_MarketPriceUpdate) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { + switch fd.FullName() { + case "layer.daemons.MarketPriceUpdate.market_id": + return protoreflect.ValueOfUint32(uint32(0)) + case "layer.daemons.MarketPriceUpdate.exchange_prices": + list := []*ExchangePrice{} + return protoreflect.ValueOfList(&_MarketPriceUpdate_2_list{list: &list}) + default: + if fd.IsExtension() { + panic(fmt.Errorf("proto3 declared messages do not support extensions: layer.daemons.MarketPriceUpdate")) + } + panic(fmt.Errorf("message layer.daemons.MarketPriceUpdate does not contain field %s", fd.FullName())) + } +} + +// WhichOneof reports which field within the oneof is populated, +// returning nil if none are populated. +// It panics if the oneof descriptor does not belong to this message. +func (x *fastReflection_MarketPriceUpdate) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor { + switch d.FullName() { + default: + panic(fmt.Errorf("%s is not a oneof field in layer.daemons.MarketPriceUpdate", d.FullName())) + } + panic("unreachable") +} + +// GetUnknown retrieves the entire list of unknown fields. +// The caller may only mutate the contents of the RawFields +// if the mutated bytes are stored back into the message with SetUnknown. +func (x *fastReflection_MarketPriceUpdate) GetUnknown() protoreflect.RawFields { + return x.unknownFields +} + +// SetUnknown stores an entire list of unknown fields. +// The raw fields must be syntactically valid according to the wire format. +// An implementation may panic if this is not the case. +// Once stored, the caller must not mutate the content of the RawFields. +// An empty RawFields may be passed to clear the fields. +// +// SetUnknown is a mutating operation and unsafe for concurrent use. +func (x *fastReflection_MarketPriceUpdate) SetUnknown(fields protoreflect.RawFields) { + x.unknownFields = fields +} + +// IsValid reports whether the message is valid. +// +// An invalid message is an empty, read-only value. +// +// An invalid message often corresponds to a nil pointer of the concrete +// message type, but the details are implementation dependent. +// Validity is not part of the protobuf data model, and may not +// be preserved in marshaling or other operations. +func (x *fastReflection_MarketPriceUpdate) IsValid() bool { + return x != nil +} + +// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations. +// This method may return nil. +// +// The returned methods type is identical to +// "google.golang.org/protobuf/runtime/protoiface".Methods. +// Consult the protoiface package documentation for details. +func (x *fastReflection_MarketPriceUpdate) ProtoMethods() *protoiface.Methods { + size := func(input protoiface.SizeInput) protoiface.SizeOutput { + x := input.Message.Interface().(*MarketPriceUpdate) + if x == nil { + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: 0, + } + } + options := runtime.SizeInputToOptions(input) + _ = options + var n int + var l int + _ = l + if x.MarketId != 0 { + n += 1 + runtime.Sov(uint64(x.MarketId)) + } + if len(x.ExchangePrices) > 0 { + for _, e := range x.ExchangePrices { + l = options.Size(e) + n += 1 + l + runtime.Sov(uint64(l)) + } + } + if x.unknownFields != nil { + n += len(x.unknownFields) + } + return protoiface.SizeOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Size: n, + } + } + + marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) { + x := input.Message.Interface().(*MarketPriceUpdate) + if x == nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + options := runtime.MarshalInputToOptions(input) + _ = options + size := options.Size(x) + dAtA := make([]byte, size) + i := len(dAtA) + _ = i + var l int + _ = l + if x.unknownFields != nil { + i -= len(x.unknownFields) + copy(dAtA[i:], x.unknownFields) + } + if len(x.ExchangePrices) > 0 { + for iNdEx := len(x.ExchangePrices) - 1; iNdEx >= 0; iNdEx-- { + encoded, err := options.Marshal(x.ExchangePrices[iNdEx]) + if err != nil { + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded))) + i-- + dAtA[i] = 0x12 + } + } + if x.MarketId != 0 { + i = runtime.EncodeVarint(dAtA, i, uint64(x.MarketId)) + i-- + dAtA[i] = 0x8 + } + if input.Buf != nil { + input.Buf = append(input.Buf, dAtA...) + } else { + input.Buf = dAtA + } + return protoiface.MarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Buf: input.Buf, + }, nil + } + unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) { + x := input.Message.Interface().(*MarketPriceUpdate) + if x == nil { + return protoiface.UnmarshalOutput{ + NoUnkeyedLiterals: input.NoUnkeyedLiterals, + Flags: input.Flags, + }, nil + } + options := runtime.UnmarshalInputToOptions(input) + _ = options + dAtA := input.Buf + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MarketPriceUpdate: wiretype end group for non-group") + } + if fieldNum <= 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: MarketPriceUpdate: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field MarketId", wireType) + } + x.MarketId = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + x.MarketId |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ExchangePrices", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow + } + if iNdEx >= l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if postIndex > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + x.ExchangePrices = append(x.ExchangePrices, &ExchangePrice{}) + if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.ExchangePrices[len(x.ExchangePrices)-1]); err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := runtime.Skip(dAtA[iNdEx:]) + if err != nil { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + if !options.DiscardUnknown { + x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + } + iNdEx += skippy + } + } + + if iNdEx > l { + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF + } + return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil + } + return &protoiface.Methods{ + NoUnkeyedLiterals: struct{}{}, + Flags: protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown, + Size: size, + Marshal: marshal, + Unmarshal: unmarshal, + Merge: nil, + CheckInitialized: nil, + } +} + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.0 +// protoc (unknown) +// source: layer/daemons/pricefeed.proto + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// UpdateMarketPriceRequest is a request message updating market prices. +type UpdateMarketPricesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MarketPriceUpdates []*MarketPriceUpdate `protobuf:"bytes,1,rep,name=market_price_updates,json=marketPriceUpdates,proto3" json:"market_price_updates,omitempty"` +} + +func (x *UpdateMarketPricesRequest) Reset() { + *x = UpdateMarketPricesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateMarketPricesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateMarketPricesRequest) ProtoMessage() {} + +// Deprecated: Use UpdateMarketPricesRequest.ProtoReflect.Descriptor instead. +func (*UpdateMarketPricesRequest) Descriptor() ([]byte, []int) { + return file_layer_daemons_pricefeed_proto_rawDescGZIP(), []int{0} +} + +func (x *UpdateMarketPricesRequest) GetMarketPriceUpdates() []*MarketPriceUpdate { + if x != nil { + return x.MarketPriceUpdates + } + return nil +} + +// UpdateMarketPricesResponse is a response message for updating market prices. +type UpdateMarketPricesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UpdateMarketPricesResponse) Reset() { + *x = UpdateMarketPricesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateMarketPricesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateMarketPricesResponse) ProtoMessage() {} + +// Deprecated: Use UpdateMarketPricesResponse.ProtoReflect.Descriptor instead. +func (*UpdateMarketPricesResponse) Descriptor() ([]byte, []int) { + return file_layer_daemons_pricefeed_proto_rawDescGZIP(), []int{1} +} + +// ExchangePrice represents a specific exchange's market price +type ExchangePrice struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ExchangeId string `protobuf:"bytes,1,opt,name=exchange_id,json=exchangeId,proto3" json:"exchange_id,omitempty"` + Price uint64 `protobuf:"varint,2,opt,name=price,proto3" json:"price,omitempty"` + LastUpdateTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_update_time,json=lastUpdateTime,proto3" json:"last_update_time,omitempty"` +} + +func (x *ExchangePrice) Reset() { + *x = ExchangePrice{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExchangePrice) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangePrice) ProtoMessage() {} + +// Deprecated: Use ExchangePrice.ProtoReflect.Descriptor instead. +func (*ExchangePrice) Descriptor() ([]byte, []int) { + return file_layer_daemons_pricefeed_proto_rawDescGZIP(), []int{2} +} + +func (x *ExchangePrice) GetExchangeId() string { + if x != nil { + return x.ExchangeId + } + return "" +} + +func (x *ExchangePrice) GetPrice() uint64 { + if x != nil { + return x.Price + } + return 0 +} + +func (x *ExchangePrice) GetLastUpdateTime() *timestamppb.Timestamp { + if x != nil { + return x.LastUpdateTime + } + return nil +} + +// MarketPriceUpdate represents an update to a single market +type MarketPriceUpdate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MarketId uint32 `protobuf:"varint,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ExchangePrices []*ExchangePrice `protobuf:"bytes,2,rep,name=exchange_prices,json=exchangePrices,proto3" json:"exchange_prices,omitempty"` +} + +func (x *MarketPriceUpdate) Reset() { + *x = MarketPriceUpdate{} + if protoimpl.UnsafeEnabled { + mi := &file_layer_daemons_pricefeed_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MarketPriceUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarketPriceUpdate) ProtoMessage() {} + +// Deprecated: Use MarketPriceUpdate.ProtoReflect.Descriptor instead. +func (*MarketPriceUpdate) Descriptor() ([]byte, []int) { + return file_layer_daemons_pricefeed_proto_rawDescGZIP(), []int{3} +} + +func (x *MarketPriceUpdate) GetMarketId() uint32 { + if x != nil { + return x.MarketId + } + return 0 +} + +func (x *MarketPriceUpdate) GetExchangePrices() []*ExchangePrice { + if x != nil { + return x.ExchangePrices + } + return nil +} + +var File_layer_daemons_pricefeed_proto protoreflect.FileDescriptor + +var file_layer_daemons_pricefeed_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2f, + 0x70, 0x72, 0x69, 0x63, 0x65, 0x66, 0x65, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0d, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x1a, 0x14, + 0x67, 0x6f, 0x67, 0x6f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x67, 0x6f, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x6f, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x72, + 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x52, 0x0a, 0x14, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, + 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, + 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, + 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x12, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, + 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x96, 0x01, 0x0a, 0x0d, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, + 0x72, 0x69, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x4e, 0x0a, 0x10, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x42, 0x08, 0xc8, 0xde, 0x1f, 0x01, 0x90, 0xdf, 0x1f, 0x01, 0x52, 0x0e, 0x6c, 0x61, 0x73, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x77, 0x0a, 0x11, 0x4d, + 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x45, 0x0a, + 0x0f, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, + 0x72, 0x69, 0x63, 0x65, 0x52, 0x0e, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, + 0x69, 0x63, 0x65, 0x73, 0x32, 0x7f, 0x0a, 0x10, 0x50, 0x72, 0x69, 0x63, 0x65, 0x46, 0x65, 0x65, + 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6b, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x73, 0x12, 0x28, + 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x6c, 0x61, 0x79, 0x65, 0x72, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, + 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x98, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x6c, 0x61, + 0x79, 0x65, 0x72, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0x42, 0x0e, 0x50, 0x72, 0x69, + 0x63, 0x65, 0x66, 0x65, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x1e, 0x63, + 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x6c, 0x61, 0x79, 0x65, 0x72, 0x2f, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, 0xa2, 0x02, 0x03, + 0x4c, 0x44, 0x58, 0xaa, 0x02, 0x0d, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x2e, 0x44, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x73, 0xca, 0x02, 0x0d, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x5c, 0x44, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x73, 0xe2, 0x02, 0x19, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x5c, 0x44, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x0e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x3a, 0x3a, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_layer_daemons_pricefeed_proto_rawDescOnce sync.Once + file_layer_daemons_pricefeed_proto_rawDescData = file_layer_daemons_pricefeed_proto_rawDesc +) + +func file_layer_daemons_pricefeed_proto_rawDescGZIP() []byte { + file_layer_daemons_pricefeed_proto_rawDescOnce.Do(func() { + file_layer_daemons_pricefeed_proto_rawDescData = protoimpl.X.CompressGZIP(file_layer_daemons_pricefeed_proto_rawDescData) + }) + return file_layer_daemons_pricefeed_proto_rawDescData +} + +var file_layer_daemons_pricefeed_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_layer_daemons_pricefeed_proto_goTypes = []interface{}{ + (*UpdateMarketPricesRequest)(nil), // 0: layer.daemons.UpdateMarketPricesRequest + (*UpdateMarketPricesResponse)(nil), // 1: layer.daemons.UpdateMarketPricesResponse + (*ExchangePrice)(nil), // 2: layer.daemons.ExchangePrice + (*MarketPriceUpdate)(nil), // 3: layer.daemons.MarketPriceUpdate + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_layer_daemons_pricefeed_proto_depIdxs = []int32{ + 3, // 0: layer.daemons.UpdateMarketPricesRequest.market_price_updates:type_name -> layer.daemons.MarketPriceUpdate + 4, // 1: layer.daemons.ExchangePrice.last_update_time:type_name -> google.protobuf.Timestamp + 2, // 2: layer.daemons.MarketPriceUpdate.exchange_prices:type_name -> layer.daemons.ExchangePrice + 0, // 3: layer.daemons.PriceFeedService.UpdateMarketPrices:input_type -> layer.daemons.UpdateMarketPricesRequest + 1, // 4: layer.daemons.PriceFeedService.UpdateMarketPrices:output_type -> layer.daemons.UpdateMarketPricesResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_layer_daemons_pricefeed_proto_init() } +func file_layer_daemons_pricefeed_proto_init() { + if File_layer_daemons_pricefeed_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_layer_daemons_pricefeed_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateMarketPricesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_pricefeed_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateMarketPricesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_pricefeed_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExchangePrice); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_layer_daemons_pricefeed_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MarketPriceUpdate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_layer_daemons_pricefeed_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_layer_daemons_pricefeed_proto_goTypes, + DependencyIndexes: file_layer_daemons_pricefeed_proto_depIdxs, + MessageInfos: file_layer_daemons_pricefeed_proto_msgTypes, + }.Build() + File_layer_daemons_pricefeed_proto = out.File + file_layer_daemons_pricefeed_proto_rawDesc = nil + file_layer_daemons_pricefeed_proto_goTypes = nil + file_layer_daemons_pricefeed_proto_depIdxs = nil +} diff --git a/api/layer/daemons/pricefeed_grpc.pb.go b/api/layer/daemons/pricefeed_grpc.pb.go new file mode 100644 index 000000000..c40a3efa9 --- /dev/null +++ b/api/layer/daemons/pricefeed_grpc.pb.go @@ -0,0 +1,103 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package daemons + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// PriceFeedServiceClient is the client API for PriceFeedService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PriceFeedServiceClient interface { + // Updates market prices. + UpdateMarketPrices(ctx context.Context, in *UpdateMarketPricesRequest, opts ...grpc.CallOption) (*UpdateMarketPricesResponse, error) +} + +type priceFeedServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPriceFeedServiceClient(cc grpc.ClientConnInterface) PriceFeedServiceClient { + return &priceFeedServiceClient{cc} +} + +func (c *priceFeedServiceClient) UpdateMarketPrices(ctx context.Context, in *UpdateMarketPricesRequest, opts ...grpc.CallOption) (*UpdateMarketPricesResponse, error) { + out := new(UpdateMarketPricesResponse) + err := c.cc.Invoke(ctx, "/layer.daemons.PriceFeedService/UpdateMarketPrices", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PriceFeedServiceServer is the server API for PriceFeedService service. +// All implementations must embed UnimplementedPriceFeedServiceServer +// for forward compatibility +type PriceFeedServiceServer interface { + // Updates market prices. + UpdateMarketPrices(context.Context, *UpdateMarketPricesRequest) (*UpdateMarketPricesResponse, error) + mustEmbedUnimplementedPriceFeedServiceServer() +} + +// UnimplementedPriceFeedServiceServer must be embedded to have forward compatible implementations. +type UnimplementedPriceFeedServiceServer struct { +} + +func (UnimplementedPriceFeedServiceServer) UpdateMarketPrices(context.Context, *UpdateMarketPricesRequest) (*UpdateMarketPricesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateMarketPrices not implemented") +} +func (UnimplementedPriceFeedServiceServer) mustEmbedUnimplementedPriceFeedServiceServer() {} + +// UnsafePriceFeedServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PriceFeedServiceServer will +// result in compilation errors. +type UnsafePriceFeedServiceServer interface { + mustEmbedUnimplementedPriceFeedServiceServer() +} + +func RegisterPriceFeedServiceServer(s grpc.ServiceRegistrar, srv PriceFeedServiceServer) { + s.RegisterService(&PriceFeedService_ServiceDesc, srv) +} + +func _PriceFeedService_UpdateMarketPrices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateMarketPricesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PriceFeedServiceServer).UpdateMarketPrices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/layer.daemons.PriceFeedService/UpdateMarketPrices", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PriceFeedServiceServer).UpdateMarketPrices(ctx, req.(*UpdateMarketPricesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// PriceFeedService_ServiceDesc is the grpc.ServiceDesc for PriceFeedService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PriceFeedService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "layer.daemons.PriceFeedService", + HandlerType: (*PriceFeedServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UpdateMarketPrices", + Handler: _PriceFeedService_UpdateMarketPrices_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "layer/daemons/pricefeed.proto", +} diff --git a/daemons/flags/flags_test.go b/daemons/flags/flags_test.go index 1bddc45df..57a69221e 100644 --- a/daemons/flags/flags_test.go +++ b/daemons/flags/flags_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/tellor-io/layer/daemons/flags" - "github.com/tellor-io/layer/mocks" + "github.com/tellor-io/layer/daemons/mocks" ) func TestAddDaemonFlagsToCmd(t *testing.T) { diff --git a/daemons/metrics/client/client.go b/daemons/metrics/client/client.go index 372b5d7e8..a446aba35 100644 --- a/daemons/metrics/client/client.go +++ b/daemons/metrics/client/client.go @@ -4,10 +4,10 @@ import ( "context" "time" - gometrics "github.com/armon/go-metrics" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/telemetry" "github.com/cosmos/cosmos-sdk/version" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/lib/metrics" ) diff --git a/daemons/mocks/AppOptions.go b/daemons/mocks/AppOptions.go new file mode 100644 index 000000000..a53be4095 --- /dev/null +++ b/daemons/mocks/AppOptions.go @@ -0,0 +1,41 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// AppOptions is an autogenerated mock type for the AppOptions type +type AppOptions struct { + mock.Mock +} + +// Get provides a mock function with given fields: _a0 +func (_m *AppOptions) Get(_a0 string) interface{} { + ret := _m.Called(_a0) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(string) interface{}); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +type mockConstructorTestingTNewAppOptions interface { + mock.TestingT + Cleanup(func()) +} + +// NewAppOptions creates a new instance of AppOptions. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAppOptions(t mockConstructorTestingTNewAppOptions) *AppOptions { + mock := &AppOptions{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/BankKeeper.go b/daemons/mocks/BankKeeper.go new file mode 100644 index 000000000..0ee280a5a --- /dev/null +++ b/daemons/mocks/BankKeeper.go @@ -0,0 +1,194 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// BankKeeper is an autogenerated mock type for the BankKeeper type +type BankKeeper struct { + mock.Mock +} + +// BurnCoins provides a mock function with given fields: ctx, moduleName, amt +func (_m *BankKeeper) BurnCoins(ctx types.Context, moduleName string, amt types.Coins) error { + ret := _m.Called(ctx, moduleName, amt) + + if len(ret) == 0 { + panic("no return value specified for BurnCoins") + } + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, string, types.Coins) error); ok { + r0 = rf(ctx, moduleName, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetBalance provides a mock function with given fields: ctx, addr, denom +func (_m *BankKeeper) GetBalance(ctx types.Context, addr types.AccAddress, denom string) types.Coin { + ret := _m.Called(ctx, addr, denom) + + if len(ret) == 0 { + panic("no return value specified for GetBalance") + } + + var r0 types.Coin + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, string) types.Coin); ok { + r0 = rf(ctx, addr, denom) + } else { + r0 = ret.Get(0).(types.Coin) + } + + return r0 +} + +// GetSupply provides a mock function with given fields: ctx, denom +func (_m *BankKeeper) GetSupply(ctx types.Context, denom string) types.Coin { + ret := _m.Called(ctx, denom) + + if len(ret) == 0 { + panic("no return value specified for GetSupply") + } + + var r0 types.Coin + if rf, ok := ret.Get(0).(func(types.Context, string) types.Coin); ok { + r0 = rf(ctx, denom) + } else { + r0 = ret.Get(0).(types.Coin) + } + + return r0 +} + +// HasBalance provides a mock function with given fields: ctx, addr, amt +func (_m *BankKeeper) HasBalance(ctx types.Context, addr types.AccAddress, amt types.Coin) bool { + ret := _m.Called(ctx, addr, amt) + + if len(ret) == 0 { + panic("no return value specified for HasBalance") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.Coin) bool); ok { + r0 = rf(ctx, addr, amt) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// InputOutputCoins provides a mock function with given fields: ctx, inputs, outputs +func (_m *BankKeeper) InputOutputCoins(ctx types.Context, inputs []banktypes.Input, outputs []banktypes.Output) error { + ret := _m.Called(ctx, inputs, outputs) + + if len(ret) == 0 { + panic("no return value specified for InputOutputCoins") + } + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, []banktypes.Input, []banktypes.Output) error); ok { + r0 = rf(ctx, inputs, outputs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoinsFromAccountToModule provides a mock function with given fields: ctx, senderAddr, recipientModule, amt +func (_m *BankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + ret := _m.Called(ctx, senderAddr, recipientModule, amt) + + if len(ret) == 0 { + panic("no return value specified for SendCoinsFromAccountToModule") + } + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, string, types.Coins) error); ok { + r0 = rf(ctx, senderAddr, recipientModule, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoinsFromModuleToAccount provides a mock function with given fields: ctx, senderModule, recipientAddr, amt +func (_m *BankKeeper) SendCoinsFromModuleToAccount(ctx types.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + ret := _m.Called(ctx, senderModule, recipientAddr, amt) + + if len(ret) == 0 { + panic("no return value specified for SendCoinsFromModuleToAccount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, string, types.AccAddress, types.Coins) error); ok { + r0 = rf(ctx, senderModule, recipientAddr, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendCoinsFromModuleToModule provides a mock function with given fields: ctx, senderModule, recipientModule, amt +func (_m *BankKeeper) SendCoinsFromModuleToModule(ctx types.Context, senderModule string, recipientModule string, amt types.Coins) error { + ret := _m.Called(ctx, senderModule, recipientModule, amt) + + if len(ret) == 0 { + panic("no return value specified for SendCoinsFromModuleToModule") + } + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, string, string, types.Coins) error); ok { + r0 = rf(ctx, senderModule, recipientModule, amt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SpendableCoins provides a mock function with given fields: ctx, addr +func (_m *BankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { + ret := _m.Called(ctx, addr) + + if len(ret) == 0 { + panic("no return value specified for SpendableCoins") + } + + var r0 types.Coins + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) types.Coins); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Coins) + } + } + + return r0 +} + +// NewBankKeeper creates a new instance of BankKeeper. 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 NewBankKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *BankKeeper { + mock := &BankKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/DistrKeeper.go b/daemons/mocks/DistrKeeper.go new file mode 100644 index 000000000..6080a3ca2 --- /dev/null +++ b/daemons/mocks/DistrKeeper.go @@ -0,0 +1,79 @@ +// Code generated by mockery v2.39.1. DO NOT EDIT. + +package mocks + +import ( + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + mock "github.com/stretchr/testify/mock" + + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// DistrKeeper is an autogenerated mock type for the DistrKeeper type +type DistrKeeper struct { + mock.Mock +} + +// AllocateTokensToValidator provides a mock function with given fields: ctx, val, tokens +func (_m *DistrKeeper) AllocateTokensToValidator(ctx types.Context, val stakingtypes.ValidatorI, tokens types.DecCoins) { + _m.Called(ctx, val, tokens) +} + +// GetFeePool provides a mock function with given fields: ctx +func (_m *DistrKeeper) GetFeePool(ctx types.Context) distributiontypes.FeePool { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetFeePool") + } + + var r0 distributiontypes.FeePool + if rf, ok := ret.Get(0).(func(types.Context) distributiontypes.FeePool); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(distributiontypes.FeePool) + } + + return r0 +} + +// GetFeePoolCommunityCoins provides a mock function with given fields: ctx +func (_m *DistrKeeper) GetFeePoolCommunityCoins(ctx types.Context) types.DecCoins { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetFeePoolCommunityCoins") + } + + var r0 types.DecCoins + if rf, ok := ret.Get(0).(func(types.Context) types.DecCoins); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.DecCoins) + } + } + + return r0 +} + +// SetFeePool provides a mock function with given fields: ctx, feePool +func (_m *DistrKeeper) SetFeePool(ctx types.Context, feePool distributiontypes.FeePool) { + _m.Called(ctx, feePool) +} + +// NewDistrKeeper creates a new instance of DistrKeeper. 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 NewDistrKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *DistrKeeper { + mock := &DistrKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/ExchangeQueryHandler.go b/daemons/mocks/ExchangeQueryHandler.go new file mode 100644 index 000000000..5c3b0ef4f --- /dev/null +++ b/daemons/mocks/ExchangeQueryHandler.go @@ -0,0 +1,81 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + daemonstypes "github.com/tellor-io/layer/daemons/types" + + mock "github.com/stretchr/testify/mock" + + time "time" + + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// ExchangeQueryHandler is an autogenerated mock type for the ExchangeQueryHandler type +type ExchangeQueryHandler struct { + mock.Mock +} + +// Now provides a mock function with given fields: +func (_m *ExchangeQueryHandler) Now() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// Query provides a mock function with given fields: ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent +func (_m *ExchangeQueryHandler) Query(ctx context.Context, exchangeQueryDetails *types.ExchangeQueryDetails, exchangeConfig *types.MutableExchangeMarketConfig, marketIds []uint32, requestHandler daemonstypes.RequestHandler, marketPriceExponent map[uint32]int32) ([]*types.MarketPriceTimestamp, map[uint32]error, error) { + ret := _m.Called(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + + var r0 []*types.MarketPriceTimestamp + if rf, ok := ret.Get(0).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) []*types.MarketPriceTimestamp); ok { + r0 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.MarketPriceTimestamp) + } + } + + var r1 map[uint32]error + if rf, ok := ret.Get(1).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) map[uint32]error); ok { + r1 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(map[uint32]error) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) error); ok { + r2 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +type mockConstructorTestingTNewExchangeQueryHandler interface { + mock.TestingT + Cleanup(func()) +} + +// NewExchangeQueryHandler creates a new instance of ExchangeQueryHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExchangeQueryHandler(t mockConstructorTestingTNewExchangeQueryHandler) *ExchangeQueryHandler { + mock := &ExchangeQueryHandler{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/ExchangeToMarketPrices.go b/daemons/mocks/ExchangeToMarketPrices.go new file mode 100644 index 000000000..adcca20ce --- /dev/null +++ b/daemons/mocks/ExchangeToMarketPrices.go @@ -0,0 +1,74 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" +) + +// ExchangeToMarketPrices is an autogenerated mock type for the ExchangeToMarketPrices type +type ExchangeToMarketPrices struct { + mock.Mock +} + +// GetAllPrices provides a mock function with given fields: +func (_m *ExchangeToMarketPrices) GetAllPrices() map[string][]types.MarketPriceTimestamp { + ret := _m.Called() + + var r0 map[string][]types.MarketPriceTimestamp + if rf, ok := ret.Get(0).(func() map[string][]types.MarketPriceTimestamp); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]types.MarketPriceTimestamp) + } + } + + return r0 +} + +// GetIndexPrice provides a mock function with given fields: marketId, cutoffTime, resolver +func (_m *ExchangeToMarketPrices) GetIndexPrice(marketId uint32, cutoffTime time.Time, resolver pricefeedtypes.Resolver) (uint64, int) { + ret := _m.Called(marketId, cutoffTime, resolver) + + var r0 uint64 + if rf, ok := ret.Get(0).(func(uint32, time.Time, pricefeedtypes.Resolver) uint64); ok { + r0 = rf(marketId, cutoffTime, resolver) + } else { + r0 = ret.Get(0).(uint64) + } + + var r1 int + if rf, ok := ret.Get(1).(func(uint32, time.Time, pricefeedtypes.Resolver) int); ok { + r1 = rf(marketId, cutoffTime, resolver) + } else { + r1 = ret.Get(1).(int) + } + + return r0, r1 +} + +// UpdatePrice provides a mock function with given fields: exchangeId, marketPriceTimestamp +func (_m *ExchangeToMarketPrices) UpdatePrice(exchangeId string, marketPriceTimestamp *types.MarketPriceTimestamp) { + _m.Called(exchangeId, marketPriceTimestamp) +} + +type mockConstructorTestingTNewExchangeToMarketPrices interface { + mock.TestingT + Cleanup(func()) +} + +// NewExchangeToMarketPrices creates a new instance of ExchangeToMarketPrices. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExchangeToMarketPrices(t mockConstructorTestingTNewExchangeToMarketPrices) *ExchangeToMarketPrices { + mock := &ExchangeToMarketPrices{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/GrpcClient.go b/daemons/mocks/GrpcClient.go new file mode 100644 index 000000000..eb6a39dae --- /dev/null +++ b/daemons/mocks/GrpcClient.go @@ -0,0 +1,91 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" +) + +// GrpcClient is an autogenerated mock type for the GrpcClient type +type GrpcClient struct { + mock.Mock +} + +// CloseConnection provides a mock function with given fields: grpcConn +func (_m *GrpcClient) CloseConnection(grpcConn *grpc.ClientConn) error { + ret := _m.Called(grpcConn) + + var r0 error + if rf, ok := ret.Get(0).(func(*grpc.ClientConn) error); ok { + r0 = rf(grpcConn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewGrpcConnection provides a mock function with given fields: ctx, socketAddress +func (_m *GrpcClient) NewGrpcConnection(ctx context.Context, socketAddress string) (*grpc.ClientConn, error) { + ret := _m.Called(ctx, socketAddress) + + var r0 *grpc.ClientConn + if rf, ok := ret.Get(0).(func(context.Context, string) *grpc.ClientConn); ok { + r0 = rf(ctx, socketAddress) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*grpc.ClientConn) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, socketAddress) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewTcpConnection provides a mock function with given fields: ctx, endpoint +func (_m *GrpcClient) NewTcpConnection(ctx context.Context, endpoint string) (*grpc.ClientConn, error) { + ret := _m.Called(ctx, endpoint) + + var r0 *grpc.ClientConn + if rf, ok := ret.Get(0).(func(context.Context, string) *grpc.ClientConn); ok { + r0 = rf(ctx, endpoint) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*grpc.ClientConn) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, endpoint) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewGrpcClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewGrpcClient creates a new instance of GrpcClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewGrpcClient(t mockConstructorTestingTNewGrpcClient) *GrpcClient { + mock := &GrpcClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/Logger.go b/daemons/mocks/Logger.go new file mode 100644 index 000000000..89805e383 --- /dev/null +++ b/daemons/mocks/Logger.go @@ -0,0 +1,94 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + log "cosmossdk.io/log" + mock "github.com/stretchr/testify/mock" +) + +// Logger is an autogenerated mock type for the Logger type +type Logger struct { + mock.Mock +} + +// Debug provides a mock function with given fields: msg, keyVals +func (_m *Logger) Debug(msg string, keyVals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyVals...) + _m.Called(_ca...) +} + +// Error provides a mock function with given fields: msg, keyVals +func (_m *Logger) Error(msg string, keyVals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyVals...) + _m.Called(_ca...) +} + +// Impl provides a mock function with given fields: +func (_m *Logger) Impl() interface{} { + ret := _m.Called() + + var r0 interface{} + if rf, ok := ret.Get(0).(func() interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// Info provides a mock function with given fields: msg, keyVals +func (_m *Logger) Info(msg string, keyVals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyVals...) + _m.Called(_ca...) +} + +// Warn provides a mock function with given fields: msg, keyVals +func (_m *Logger) Warn(msg string, keyVals ...interface{}) { + var _ca []interface{} + _ca = append(_ca, msg) + _ca = append(_ca, keyVals...) + _m.Called(_ca...) +} + +// With provides a mock function with given fields: keyVals +func (_m *Logger) With(keyVals ...interface{}) log.Logger { + var _ca []interface{} + _ca = append(_ca, keyVals...) + ret := _m.Called(_ca...) + + var r0 log.Logger + if rf, ok := ret.Get(0).(func(...interface{}) log.Logger); ok { + r0 = rf(keyVals...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(log.Logger) + } + } + + return r0 +} + +type mockConstructorTestingTNewLogger interface { + mock.TestingT + Cleanup(func()) +} + +// NewLogger creates a new instance of Logger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewLogger(t mockConstructorTestingTNewLogger) *Logger { + mock := &Logger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/OracleKeeper.go b/daemons/mocks/OracleKeeper.go new file mode 100644 index 000000000..8f81d10a1 --- /dev/null +++ b/daemons/mocks/OracleKeeper.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.37.1. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + oracletypes "github.com/tellor-io/layer/x/oracle/types" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// OracleKeeper is an autogenerated mock type for the OracleKeeper type +type OracleKeeper struct { + mock.Mock +} + +// GetTotalTips provides a mock function with given fields: ctx +func (_m *OracleKeeper) GetTotalTips(ctx types.Context) types.Coin { + ret := _m.Called(ctx) + + var r0 types.Coin + if rf, ok := ret.Get(0).(func(types.Context) types.Coin); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(types.Coin) + } + + return r0 +} + +// GetUserTips provides a mock function with given fields: ctx, tipper +func (_m *OracleKeeper) GetUserTips(ctx types.Context, tipper types.AccAddress) oracletypes.UserTipTotal { + ret := _m.Called(ctx, tipper) + + var r0 oracletypes.UserTipTotal + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) oracletypes.UserTipTotal); ok { + r0 = rf(ctx, tipper) + } else { + r0 = ret.Get(0).(oracletypes.UserTipTotal) + } + + return r0 +} + +// NewOracleKeeper creates a new instance of OracleKeeper. 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 NewOracleKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *OracleKeeper { + mock := &OracleKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/PriceFeedMutableMarketConfigs.go b/daemons/mocks/PriceFeedMutableMarketConfigs.go new file mode 100644 index 000000000..f231c0c76 --- /dev/null +++ b/daemons/mocks/PriceFeedMutableMarketConfigs.go @@ -0,0 +1,108 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" + pricestypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" + mock "github.com/stretchr/testify/mock" +) + +// PricefeedMutableMarketConfigs is an autogenerated mock type for the PricefeedMutableMarketConfigs type +type PricefeedMutableMarketConfigs struct { + mock.Mock +} + +// AddPriceEncoder provides a mock function with given fields: updater +func (_m *PricefeedMutableMarketConfigs) AddPriceEncoder(updater types.ExchangeConfigUpdater) { + _m.Called(updater) +} + +// AddPriceFetcher provides a mock function with given fields: updater +func (_m *PricefeedMutableMarketConfigs) AddPriceFetcher(updater types.ExchangeConfigUpdater) { + _m.Called(updater) +} + +// GetExchangeMarketConfigCopy provides a mock function with given fields: id +func (_m *PricefeedMutableMarketConfigs) GetExchangeMarketConfigCopy(id string) (*types.MutableExchangeMarketConfig, error) { + ret := _m.Called(id) + + var r0 *types.MutableExchangeMarketConfig + if rf, ok := ret.Get(0).(func(string) *types.MutableExchangeMarketConfig); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.MutableExchangeMarketConfig) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMarketConfigCopies provides a mock function with given fields: markets +func (_m *PricefeedMutableMarketConfigs) GetMarketConfigCopies(markets []uint32) ([]*types.MutableMarketConfig, error) { + ret := _m.Called(markets) + + var r0 []*types.MutableMarketConfig + if rf, ok := ret.Get(0).(func([]uint32) []*types.MutableMarketConfig); ok { + r0 = rf(markets) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.MutableMarketConfig) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]uint32) error); ok { + r1 = rf(markets) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateMarkets provides a mock function with given fields: marketParams +func (_m *PricefeedMutableMarketConfigs) UpdateMarkets(marketParams []pricestypes.MarketParam) (map[uint32]error, error) { + ret := _m.Called(marketParams) + + var r0 map[uint32]error + if rf, ok := ret.Get(0).(func([]pricestypes.MarketParam) map[uint32]error); ok { + r0 = rf(marketParams) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[uint32]error) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]pricestypes.MarketParam) error); ok { + r1 = rf(marketParams) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewPricefeedMutableMarketConfigs interface { + mock.TestingT + Cleanup(func()) +} + +// NewPricefeedMutableMarketConfigs creates a new instance of PricefeedMutableMarketConfigs. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewPricefeedMutableMarketConfigs(t mockConstructorTestingTNewPricefeedMutableMarketConfigs) *PricefeedMutableMarketConfigs { + mock := &PricefeedMutableMarketConfigs{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/QueryClient.go b/daemons/mocks/QueryClient.go new file mode 100644 index 000000000..82db44be0 --- /dev/null +++ b/daemons/mocks/QueryClient.go @@ -0,0 +1,69 @@ +// 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/tellor-io/layer/daemons/server/types" +) + +// QueryClient is an autogenerated mock type for the PriceFeedServiceClient type +type QueryClient struct { + mock.Mock +} + +// UpdateMarketPrices provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) UpdateMarketPrices(ctx context.Context, in *types.UpdateMarketPricesRequest, opts ...grpc.CallOption) (*types.UpdateMarketPricesResponse, 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 UpdateMarketPrices") + } + + var r0 *types.UpdateMarketPricesResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) (*types.UpdateMarketPricesResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) *types.UpdateMarketPricesResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.UpdateMarketPricesResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewQueryClient creates a new instance of QueryClient. 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 NewQueryClient(t interface { + mock.TestingT + Cleanup(func()) +}) *QueryClient { + mock := &QueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/RequestHandler.go b/daemons/mocks/RequestHandler.go new file mode 100644 index 000000000..6996f58d2 --- /dev/null +++ b/daemons/mocks/RequestHandler.go @@ -0,0 +1,53 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// RequestHandler is an autogenerated mock type for the RequestHandler type +type RequestHandler struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, url +func (_m *RequestHandler) Get(ctx context.Context, url string) (*http.Response, error) { + ret := _m.Called(ctx, url) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(context.Context, string) *http.Response); ok { + r0 = rf(ctx, url) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, url) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewRequestHandler interface { + mock.TestingT + Cleanup(func()) +} + +// NewRequestHandler creates a new instance of RequestHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRequestHandler(t mockConstructorTestingTNewRequestHandler) *RequestHandler { + mock := &RequestHandler{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/SlashingKeeper.go b/daemons/mocks/SlashingKeeper.go new file mode 100644 index 000000000..ebee3ccf1 --- /dev/null +++ b/daemons/mocks/SlashingKeeper.go @@ -0,0 +1,65 @@ +// Code generated by mockery v2.36.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// SlashingKeeper is an autogenerated mock type for the SlashingKeeper type +type SlashingKeeper struct { + mock.Mock +} + +// GetValidatorSigningInfo provides a mock function with given fields: ctx, address +func (_m *SlashingKeeper) GetValidatorSigningInfo(ctx types.Context, address types.ConsAddress) (slashingtypes.ValidatorSigningInfo, bool) { + ret := _m.Called(ctx, address) + + var r0 slashingtypes.ValidatorSigningInfo + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) (slashingtypes.ValidatorSigningInfo, bool)); ok { + return rf(ctx, address) + } + if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) slashingtypes.ValidatorSigningInfo); ok { + r0 = rf(ctx, address) + } else { + r0 = ret.Get(0).(slashingtypes.ValidatorSigningInfo) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.ConsAddress) bool); ok { + r1 = rf(ctx, address) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// JailUntil provides a mock function with given fields: ctx, consAddr, jailTime +func (_m *SlashingKeeper) JailUntil(ctx types.Context, consAddr types.ConsAddress, jailTime time.Time) { + _m.Called(ctx, consAddr, jailTime) +} + +// SetValidatorSigningInfo provides a mock function with given fields: ctx, address, info +func (_m *SlashingKeeper) SetValidatorSigningInfo(ctx types.Context, address types.ConsAddress, info slashingtypes.ValidatorSigningInfo) { + _m.Called(ctx, address, info) +} + +// NewSlashingKeeper creates a new instance of SlashingKeeper. 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 NewSlashingKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *SlashingKeeper { + mock := &SlashingKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/StakingKeeper.go b/daemons/mocks/StakingKeeper.go new file mode 100644 index 000000000..926e43595 --- /dev/null +++ b/daemons/mocks/StakingKeeper.go @@ -0,0 +1,292 @@ +// Code generated by mockery v2.36.0. DO NOT EDIT. + +package mocks + +import ( + math "cosmossdk.io/math" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + mock "github.com/stretchr/testify/mock" + + types "github.com/cosmos/cosmos-sdk/types" +) + +// StakingKeeper is an autogenerated mock type for the StakingKeeper type +type StakingKeeper struct { + mock.Mock +} + +// AddValidatorTokensAndShares provides a mock function with given fields: ctx, validator, tokensToAdd +func (_m *StakingKeeper) AddValidatorTokensAndShares(ctx types.Context, validator stakingtypes.Validator, tokensToAdd math.Int) (stakingtypes.Validator, math.LegacyDec) { + ret := _m.Called(ctx, validator, tokensToAdd) + + var r0 stakingtypes.Validator + var r1 math.LegacyDec + if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.Int) (stakingtypes.Validator, math.LegacyDec)); ok { + return rf(ctx, validator, tokensToAdd) + } + if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.Int) stakingtypes.Validator); ok { + r0 = rf(ctx, validator, tokensToAdd) + } else { + r0 = ret.Get(0).(stakingtypes.Validator) + } + + if rf, ok := ret.Get(1).(func(types.Context, stakingtypes.Validator, math.Int) math.LegacyDec); ok { + r1 = rf(ctx, validator, tokensToAdd) + } else { + r1 = ret.Get(1).(math.LegacyDec) + } + + return r0, r1 +} + +// Delegate provides a mock function with given fields: ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount +func (_m *StakingKeeper) Delegate(ctx types.Context, delAddr types.AccAddress, bondAmt math.Int, tokenSrc stakingtypes.BondStatus, validator stakingtypes.Validator, subtractAccount bool) (math.LegacyDec, error) { + ret := _m.Called(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) + + var r0 math.LegacyDec + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, math.Int, stakingtypes.BondStatus, stakingtypes.Validator, bool) (math.LegacyDec, error)); ok { + return rf(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) + } + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, math.Int, stakingtypes.BondStatus, stakingtypes.Validator, bool) math.LegacyDec); ok { + r0 = rf(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) + } else { + r0 = ret.Get(0).(math.LegacyDec) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.AccAddress, math.Int, stakingtypes.BondStatus, stakingtypes.Validator, bool) error); ok { + r1 = rf(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteValidatorByPowerIndex provides a mock function with given fields: ctx, validator +func (_m *StakingKeeper) DeleteValidatorByPowerIndex(ctx types.Context, validator stakingtypes.Validator) { + _m.Called(ctx, validator) +} + +// GetAllDelegatorDelegations provides a mock function with given fields: ctx, delegator +func (_m *StakingKeeper) GetAllDelegatorDelegations(ctx types.Context, delegator types.AccAddress) []stakingtypes.Delegation { + ret := _m.Called(ctx, delegator) + + var r0 []stakingtypes.Delegation + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) []stakingtypes.Delegation); ok { + r0 = rf(ctx, delegator) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]stakingtypes.Delegation) + } + } + + return r0 +} + +// GetDelegation provides a mock function with given fields: ctx, delAddr, valAddr +func (_m *StakingKeeper) GetDelegation(ctx types.Context, delAddr types.AccAddress, valAddr types.ValAddress) (stakingtypes.Delegation, bool) { + ret := _m.Called(ctx, delAddr, valAddr) + + var r0 stakingtypes.Delegation + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.ValAddress) (stakingtypes.Delegation, bool)); ok { + return rf(ctx, delAddr, valAddr) + } + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.ValAddress) stakingtypes.Delegation); ok { + r0 = rf(ctx, delAddr, valAddr) + } else { + r0 = ret.Get(0).(stakingtypes.Delegation) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.AccAddress, types.ValAddress) bool); ok { + r1 = rf(ctx, delAddr, valAddr) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// GetLastTotalPower provides a mock function with given fields: ctx +func (_m *StakingKeeper) GetLastTotalPower(ctx types.Context) math.Int { + ret := _m.Called(ctx) + + var r0 math.Int + if rf, ok := ret.Get(0).(func(types.Context) math.Int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(math.Int) + } + + return r0 +} + +// GetValidator provides a mock function with given fields: ctx, addr +func (_m *StakingKeeper) GetValidator(ctx types.Context, addr types.ValAddress) (stakingtypes.Validator, bool) { + ret := _m.Called(ctx, addr) + + var r0 stakingtypes.Validator + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, types.ValAddress) (stakingtypes.Validator, bool)); ok { + return rf(ctx, addr) + } + if rf, ok := ret.Get(0).(func(types.Context, types.ValAddress) stakingtypes.Validator); ok { + r0 = rf(ctx, addr) + } else { + r0 = ret.Get(0).(stakingtypes.Validator) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.ValAddress) bool); ok { + r1 = rf(ctx, addr) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// GetValidatorByConsAddr provides a mock function with given fields: ctx, consAddr +func (_m *StakingKeeper) GetValidatorByConsAddr(ctx types.Context, consAddr types.ConsAddress) (stakingtypes.Validator, bool) { + ret := _m.Called(ctx, consAddr) + + var r0 stakingtypes.Validator + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) (stakingtypes.Validator, bool)); ok { + return rf(ctx, consAddr) + } + if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) stakingtypes.Validator); ok { + r0 = rf(ctx, consAddr) + } else { + r0 = ret.Get(0).(stakingtypes.Validator) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.ConsAddress) bool); ok { + r1 = rf(ctx, consAddr) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// IterateDelegatorDelegations provides a mock function with given fields: ctx, delegator, cb +func (_m *StakingKeeper) IterateDelegatorDelegations(ctx types.Context, delegator types.AccAddress, cb func(stakingtypes.Delegation) bool) { + _m.Called(ctx, delegator, cb) +} + +// Jail provides a mock function with given fields: ctx, consAddr +func (_m *StakingKeeper) Jail(ctx types.Context, consAddr types.ConsAddress) { + _m.Called(ctx, consAddr) +} + +// RemoveDelegation provides a mock function with given fields: ctx, delegation +func (_m *StakingKeeper) RemoveDelegation(ctx types.Context, delegation stakingtypes.Delegation) error { + ret := _m.Called(ctx, delegation) + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Delegation) error); ok { + r0 = rf(ctx, delegation) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveValidatorTokens provides a mock function with given fields: ctx, validator, tokensToRemove +func (_m *StakingKeeper) RemoveValidatorTokens(ctx types.Context, validator stakingtypes.Validator, tokensToRemove math.Int) stakingtypes.Validator { + ret := _m.Called(ctx, validator, tokensToRemove) + + var r0 stakingtypes.Validator + if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.Int) stakingtypes.Validator); ok { + r0 = rf(ctx, validator, tokensToRemove) + } else { + r0 = ret.Get(0).(stakingtypes.Validator) + } + + return r0 +} + +// RemoveValidatorTokensAndShares provides a mock function with given fields: ctx, validator, sharesToRemove +func (_m *StakingKeeper) RemoveValidatorTokensAndShares(ctx types.Context, validator stakingtypes.Validator, sharesToRemove math.LegacyDec) (stakingtypes.Validator, math.Int) { + ret := _m.Called(ctx, validator, sharesToRemove) + + var r0 stakingtypes.Validator + var r1 math.Int + if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.LegacyDec) (stakingtypes.Validator, math.Int)); ok { + return rf(ctx, validator, sharesToRemove) + } + if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.LegacyDec) stakingtypes.Validator); ok { + r0 = rf(ctx, validator, sharesToRemove) + } else { + r0 = ret.Get(0).(stakingtypes.Validator) + } + + if rf, ok := ret.Get(1).(func(types.Context, stakingtypes.Validator, math.LegacyDec) math.Int); ok { + r1 = rf(ctx, validator, sharesToRemove) + } else { + r1 = ret.Get(1).(math.Int) + } + + return r0, r1 +} + +// SetDelegation provides a mock function with given fields: ctx, delegation +func (_m *StakingKeeper) SetDelegation(ctx types.Context, delegation stakingtypes.Delegation) { + _m.Called(ctx, delegation) +} + +// SetValidator provides a mock function with given fields: ctx, validator +func (_m *StakingKeeper) SetValidator(ctx types.Context, validator stakingtypes.Validator) { + _m.Called(ctx, validator) +} + +// SetValidatorByPowerIndex provides a mock function with given fields: ctx, validator +func (_m *StakingKeeper) SetValidatorByPowerIndex(ctx types.Context, validator stakingtypes.Validator) { + _m.Called(ctx, validator) +} + +// TokensFromConsensusPower provides a mock function with given fields: ctx, power +func (_m *StakingKeeper) TokensFromConsensusPower(ctx types.Context, power int64) math.Int { + ret := _m.Called(ctx, power) + + var r0 math.Int + if rf, ok := ret.Get(0).(func(types.Context, int64) math.Int); ok { + r0 = rf(ctx, power) + } else { + r0 = ret.Get(0).(math.Int) + } + + return r0 +} + +// Validator provides a mock function with given fields: ctx, address +func (_m *StakingKeeper) Validator(ctx types.Context, address types.ValAddress) stakingtypes.ValidatorI { + ret := _m.Called(ctx, address) + + var r0 stakingtypes.ValidatorI + if rf, ok := ret.Get(0).(func(types.Context, types.ValAddress) stakingtypes.ValidatorI); ok { + r0 = rf(ctx, address) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(stakingtypes.ValidatorI) + } + } + + return r0 +} + +// NewStakingKeeper creates a new instance of StakingKeeper. 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 NewStakingKeeper(t interface { + mock.TestingT + Cleanup(func()) +}) *StakingKeeper { + mock := &StakingKeeper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/mocks/TimeProvider.go b/daemons/mocks/TimeProvider.go new file mode 100644 index 000000000..c5db9b121 --- /dev/null +++ b/daemons/mocks/TimeProvider.go @@ -0,0 +1,43 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" +) + +// TimeProvider is an autogenerated mock type for the TimeProvider type +type TimeProvider struct { + mock.Mock +} + +// Now provides a mock function with given fields: +func (_m *TimeProvider) Now() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +type mockConstructorTestingTNewTimeProvider interface { + mock.TestingT + Cleanup(func()) +} + +// NewTimeProvider creates a new instance of TimeProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewTimeProvider(t mockConstructorTestingTNewTimeProvider) *TimeProvider { + mock := &TimeProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/daemons/pricefeed/client/client.go b/daemons/pricefeed/client/client.go index b9605e2a2..04ff0e450 100644 --- a/daemons/pricefeed/client/client.go +++ b/daemons/pricefeed/client/client.go @@ -10,8 +10,7 @@ import ( appflags "github.com/tellor-io/layer/app/flags" daemontypes "github.com/tellor-io/layer/daemons/types" - sdklog "cosmossdk.io/log" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/tellor-io/layer/daemons/constants" "github.com/tellor-io/layer/daemons/flags" "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" @@ -59,7 +58,7 @@ type Client struct { var _ daemontypes.HealthCheckable = (*Client)(nil) func newClient(logger log.Logger) *Client { - logger = logger.With(sdklog.ModuleKey, constants.PricefeedDaemonModuleName) + logger = logger.With(log.ModuleKey, constants.PricefeedDaemonModuleName) client := &Client{ tickers: []*time.Ticker{}, stops: []chan bool{}, diff --git a/daemons/pricefeed/client/client_test.go b/daemons/pricefeed/client/client_test.go index 04479bf16..b261d5201 100644 --- a/daemons/pricefeed/client/client_test.go +++ b/daemons/pricefeed/client/client_test.go @@ -27,12 +27,12 @@ import ( servertypes "github.com/tellor-io/layer/daemons/server/types" "google.golang.org/grpc" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/mocks" "github.com/tellor-io/layer/daemons/pricefeed/client/types" pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" - "github.com/tellor-io/layer/mocks" "github.com/tellor-io/layer/testutil/client" "github.com/tellor-io/layer/testutil/constants" diff --git a/daemons/pricefeed/client/price_encoder/price_encoder.go b/daemons/pricefeed/client/price_encoder/price_encoder.go index f0594fb26..6e3b78d80 100644 --- a/daemons/pricefeed/client/price_encoder/price_encoder.go +++ b/daemons/pricefeed/client/price_encoder/price_encoder.go @@ -7,9 +7,9 @@ import ( "syscall" "time" - gometrics "github.com/armon/go-metrics" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/daemons/constants" "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" price_function "github.com/tellor-io/layer/daemons/pricefeed/client/sources" diff --git a/daemons/pricefeed/client/price_encoder/price_encoder_test.go b/daemons/pricefeed/client/price_encoder/price_encoder_test.go index b210b5870..16ea270d1 100644 --- a/daemons/pricefeed/client/price_encoder/price_encoder_test.go +++ b/daemons/pricefeed/client/price_encoder/price_encoder_test.go @@ -8,16 +8,16 @@ import ( "testing" "time" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" pf_constants "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/mocks" "github.com/tellor-io/layer/daemons/pricefeed/client/price_fetcher" "github.com/tellor-io/layer/daemons/pricefeed/client/sources" "github.com/tellor-io/layer/daemons/pricefeed/client/types" pft "github.com/tellor-io/layer/daemons/pricefeed/types" "github.com/tellor-io/layer/lib/metrics" - "github.com/tellor-io/layer/mocks" "github.com/tellor-io/layer/testutil/constants" ) @@ -47,7 +47,7 @@ func genNewPriceEncoder(t *testing.T) *PriceEncoderImpl { &constants.Exchange1_3Markets_MutableExchangeMarketConfig, constants.MutableMarketConfigs_3Markets, etmp, - log.TestingLogger(), + log.NewTestLogger(t), bCh, ) require.NoError(t, err) @@ -262,7 +262,7 @@ func TestConvertPriceUpdate_Mixed(t *testing.T) { tc.mutableExchangeConfig, tc.mutableMarketConfigs, &emtp, - log.TestingLogger(), + log.NewTestLogger(t), nil, ) require.NoError(t, err) diff --git a/daemons/pricefeed/client/price_fetcher/mutable_state_test.go b/daemons/pricefeed/client/price_fetcher/mutable_state_test.go index 22f8a0228..5dcad2778 100644 --- a/daemons/pricefeed/client/price_fetcher/mutable_state_test.go +++ b/daemons/pricefeed/client/price_fetcher/mutable_state_test.go @@ -3,10 +3,10 @@ package price_fetcher import ( "testing" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/mocks" "github.com/tellor-io/layer/daemons/pricefeed/client/types" - "github.com/tellor-io/layer/mocks" "github.com/tellor-io/layer/testutil/constants" ) @@ -17,7 +17,7 @@ func TestGetNextNMarkets(t *testing.T) { &constants.Exchange1_3Markets_MutableExchangeMarketConfig, constants.MutableMarketConfigs_3Markets, &mocks.ExchangeQueryHandler{}, - log.NewNopLogger(), + log.NewTestLogger(t), newTestPriceFetcherBufferedChannel(), ) require.NoError(t, err) diff --git a/daemons/pricefeed/client/price_fetcher/price_fetcher.go b/daemons/pricefeed/client/price_fetcher/price_fetcher.go index 14e3a709d..5a9cba384 100644 --- a/daemons/pricefeed/client/price_fetcher/price_fetcher.go +++ b/daemons/pricefeed/client/price_fetcher/price_fetcher.go @@ -10,8 +10,8 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" daemontypes "github.com/tellor-io/layer/daemons/types" - gometrics "github.com/armon/go-metrics" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/daemons/constants" handler "github.com/tellor-io/layer/daemons/pricefeed/client/queryhandler" "github.com/tellor-io/layer/daemons/pricefeed/client/types" diff --git a/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go b/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go index ab35a8521..c6f73204b 100644 --- a/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go +++ b/daemons/pricefeed/client/price_fetcher/price_fetcher_test.go @@ -10,12 +10,12 @@ import ( pricefeed_cosntants "github.com/tellor-io/layer/daemons/constants" "github.com/tellor-io/layer/testutil/daemons/pricefeed" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/tellor-io/layer/daemons/mocks" "github.com/tellor-io/layer/daemons/pricefeed/client/types" "github.com/tellor-io/layer/lib" - "github.com/tellor-io/layer/mocks" "github.com/tellor-io/layer/testutil/constants" ) diff --git a/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go b/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go index be9e7e280..95cbfc282 100644 --- a/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go +++ b/daemons/pricefeed/client/queryhandler/exchange_query_handler_test.go @@ -13,10 +13,10 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" pf_constants "github.com/tellor-io/layer/daemons/constants" + "github.com/tellor-io/layer/daemons/mocks" "github.com/tellor-io/layer/daemons/pricefeed/client/sources" "github.com/tellor-io/layer/daemons/pricefeed/client/types" pft "github.com/tellor-io/layer/daemons/pricefeed/types" - "github.com/tellor-io/layer/mocks" "github.com/tellor-io/layer/testutil/constants" "github.com/tellor-io/layer/testutil/daemons/pricefeed" ) diff --git a/daemons/pricefeed/client/subtask_runner.go b/daemons/pricefeed/client/subtask_runner.go index 0b0992f1d..632e648bd 100644 --- a/daemons/pricefeed/client/subtask_runner.go +++ b/daemons/pricefeed/client/subtask_runner.go @@ -13,7 +13,7 @@ import ( daemontypes "github.com/tellor-io/layer/daemons/types" "github.com/tellor-io/layer/lib/metrics" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" handler "github.com/tellor-io/layer/daemons/pricefeed/client/queryhandler" "github.com/tellor-io/layer/daemons/pricefeed/client/types" servertypes "github.com/tellor-io/layer/daemons/server/types" diff --git a/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go b/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go index 11254480f..4d54104ba 100644 --- a/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go +++ b/daemons/pricefeed/client/types/price_feed_mutable_market_configs.go @@ -7,8 +7,8 @@ import ( "strings" "sync" - gometrics "github.com/armon/go-metrics" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/lib/metrics" ) diff --git a/daemons/pricefeed/metrics/metrics.go b/daemons/pricefeed/metrics/metrics.go index 4dded0011..cb29ce7e7 100644 --- a/daemons/pricefeed/metrics/metrics.go +++ b/daemons/pricefeed/metrics/metrics.go @@ -1,7 +1,7 @@ package metrics import ( - gometrics "github.com/armon/go-metrics" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/daemons/pricefeed/client/types" "github.com/tellor-io/layer/lib/metrics" ) diff --git a/daemons/server/median/median.go b/daemons/server/median/median.go index b32e2c73e..5e9df2ccb 100644 --- a/daemons/server/median/median.go +++ b/daemons/server/median/median.go @@ -6,7 +6,7 @@ import ( "time" "github.com/cosmos/cosmos-sdk/client" - gogogrpc "github.com/gogo/protobuf/grpc" + gogogrpc "github.com/cosmos/gogoproto/grpc" "github.com/grpc-ecosystem/grpc-gateway/runtime" pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" "github.com/tellor-io/layer/daemons/server/types" diff --git a/daemons/server/pricefeed.go b/daemons/server/pricefeed.go index 314b5e5ab..1340916c5 100644 --- a/daemons/server/pricefeed.go +++ b/daemons/server/pricefeed.go @@ -7,8 +7,8 @@ import ( errorsmod "cosmossdk.io/errors" - gometrics "github.com/armon/go-metrics" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/daemons/constants" pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" diff --git a/daemons/server/server.go b/daemons/server/server.go index 6a25a5e9a..a408c1bc9 100644 --- a/daemons/server/server.go +++ b/daemons/server/server.go @@ -4,9 +4,9 @@ import ( "net" // "syscall" - gometrics "github.com/armon/go-metrics" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/daemons/constants" "github.com/tellor-io/layer/daemons/server/types" daemontypes "github.com/tellor-io/layer/daemons/types" diff --git a/daemons/server/types/health_checker.go b/daemons/server/types/health_checker.go index d9bc77729..4e8129b05 100644 --- a/daemons/server/types/health_checker.go +++ b/daemons/server/types/health_checker.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/tellor-io/layer/daemons/types" libtime "github.com/tellor-io/layer/lib/time" ) diff --git a/daemons/server/types/health_monitor.go b/daemons/server/types/health_monitor.go index ebf5e13bb..2d93b0f5c 100644 --- a/daemons/server/types/health_monitor.go +++ b/daemons/server/types/health_monitor.go @@ -5,8 +5,7 @@ import ( "sync" "time" - cosmoslog "cosmossdk.io/log" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" "github.com/tellor-io/layer/daemons/types" libtime "github.com/tellor-io/layer/lib/time" ) @@ -142,7 +141,7 @@ func NewHealthMonitor( ) *HealthMonitor { return &HealthMonitor{ mutableState: newHealthMonitorMutableState(), - logger: logger.With(cosmoslog.ModuleKey, HealthMonitorLogModuleName), + logger: logger.With(log.ModuleKey, HealthMonitorLogModuleName), startupGracePeriod: startupGracePeriod, pollingFrequency: pollingFrequency, enablePanics: enablePanics, diff --git a/daemons/server/types/median_values.pb.gw.go b/daemons/server/types/median_values.pb.gw.go index 2e04636cb..8212c2080 100644 --- a/daemons/server/types/median_values.pb.gw.go +++ b/daemons/server/types/median_values.pb.gw.go @@ -242,9 +242,9 @@ func RegisterMedianValuesServiceHandlerClient(ctx context.Context, mux *runtime. } var ( - pattern_MedianValuesService_GetAllMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"layer", "api", "get_all_median_values"}, "", runtime.AssumeColonVerbOpt(true))) + pattern_MedianValuesService_GetAllMedianValues_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"layer", "api", "get_all_median_values"}, "", runtime.AssumeColonVerbOpt(false))) - pattern_MedianValuesService_GetMedianValue_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"layer", "api", "get_median_value", "query_data"}, "", runtime.AssumeColonVerbOpt(true))) + pattern_MedianValuesService_GetMedianValue_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"layer", "api", "get_median_value", "query_data"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( diff --git a/daemons/server/types/pricefeed/exchange_to_price.go b/daemons/server/types/pricefeed/exchange_to_price.go index f8a9600e2..136d358ad 100644 --- a/daemons/server/types/pricefeed/exchange_to_price.go +++ b/daemons/server/types/pricefeed/exchange_to_price.go @@ -3,8 +3,8 @@ package types import ( "time" - gometrics "github.com/armon/go-metrics" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" "github.com/tellor-io/layer/daemons/pricefeed/types" servertypes "github.com/tellor-io/layer/daemons/server/types" diff --git a/daemons/server/types/pricefeed/market_to_exchange_prices.go b/daemons/server/types/pricefeed/market_to_exchange_prices.go index b9991ab0e..967273abe 100644 --- a/daemons/server/types/pricefeed/market_to_exchange_prices.go +++ b/daemons/server/types/pricefeed/market_to_exchange_prices.go @@ -4,8 +4,8 @@ import ( "sync" "time" - gometrics "github.com/armon/go-metrics" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/daemons/pricefeed/client/types" pricefeedmetrics "github.com/tellor-io/layer/daemons/pricefeed/metrics" servertypes "github.com/tellor-io/layer/daemons/server/types" diff --git a/daemons/types/health_checkable.go b/daemons/types/health_checkable.go index 85566805f..7403e8958 100644 --- a/daemons/types/health_checkable.go +++ b/daemons/types/health_checkable.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/cometbft/cometbft/libs/log" + "cosmossdk.io/log" libtime "github.com/tellor-io/layer/lib/time" ) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index 5c1d506b7..cdd928983 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -17767,6 +17767,108 @@ paths: parameters. tags: - Msg + /layer/api/get_all_median_values: + get: + summary: GetAllMedianValues returns the median values for all markets + operationId: LayerDaemonsGetAllMedianValues + responses: + '200': + description: A successful response. + schema: + type: object + properties: + median_values: + type: array + items: + type: object + properties: + market_id: + type: integer + format: int64 + title: marketId is the market id for a pair + price: + type: string + format: uint64 + exponent: + type: integer + format: int32 + title: MedianValues is the median value for a market + title: >- + GetAllMedianValuesResponse is the response for the + GetAllMedianValues rpc + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + tags: + - MedianValuesService + /layer/api/get_median_value/{query_data}: + get: + summary: >- + GetMedianValue returns the median value for a market given a query data + that a market is associated with + operationId: LayerDaemonsGetMedianValue + responses: + '200': + description: A successful response. + schema: + type: object + properties: + median_values: + type: object + properties: + market_id: + type: integer + format: int64 + title: marketId is the market id for a pair + price: + type: string + format: uint64 + exponent: + type: integer + format: int32 + title: MedianValues is the median value for a market + title: GetMedianValueResponse is the response for the GetMedianValue rpc + default: + description: An unexpected error response. + schema: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + '@type': + type: string + additionalProperties: {} + parameters: + - name: query_data + description: query data to fetch prices for + in: path + required: true + type: string + tags: + - MedianValuesService /layer/oracle/get_reportsby_qid/{queryId}: get: summary: Queries a list of GetReportsbyQid items. @@ -25511,6 +25613,96 @@ definitions: description: |- Version defines the versioning scheme used to negotiate the IBC verison in the connection handshake. + layer.daemons.GetAllMedianValuesResponse: + type: object + properties: + median_values: + type: array + items: + type: object + properties: + market_id: + type: integer + format: int64 + title: marketId is the market id for a pair + price: + type: string + format: uint64 + exponent: + type: integer + format: int32 + title: MedianValues is the median value for a market + title: GetAllMedianValuesResponse is the response for the GetAllMedianValues rpc + layer.daemons.GetMedianValueResponse: + type: object + properties: + median_values: + type: object + properties: + market_id: + type: integer + format: int64 + title: marketId is the market id for a pair + price: + type: string + format: uint64 + exponent: + type: integer + format: int32 + title: MedianValues is the median value for a market + title: GetMedianValueResponse is the response for the GetMedianValue rpc + layer.daemons.MedianValues: + type: object + properties: + market_id: + type: integer + format: int64 + title: marketId is the market id for a pair + price: + type: string + format: uint64 + exponent: + type: integer + format: int32 + title: MedianValues is the median value for a market + layer.daemons.ExchangePrice: + type: object + properties: + exchange_id: + type: string + price: + type: string + format: uint64 + last_update_time: + type: string + format: date-time + title: ExchangePrice represents a specific exchange's market price + layer.daemons.MarketPriceUpdate: + type: object + properties: + market_id: + type: integer + format: int64 + exchange_prices: + type: array + items: + type: object + properties: + exchange_id: + type: string + price: + type: string + format: uint64 + last_update_time: + type: string + format: date-time + title: ExchangePrice represents a specific exchange's market price + title: MarketPriceUpdate represents an update to a single market + layer.daemons.UpdateMarketPricesResponse: + type: object + description: >- + UpdateMarketPricesResponse is a response message for updating market + prices. layer.oracle.Aggregate: type: object properties: diff --git a/go.mod b/go.mod index 906fcd5d5..e4739c6ea 100644 --- a/go.mod +++ b/go.mod @@ -28,20 +28,24 @@ require ( github.com/cosmos/ibc-go/v8 v8.0.0 github.com/ethereum/go-ethereum v1.10.22 github.com/go-playground/validator/v10 v10.12.0 - github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.3 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 + github.com/hashicorp/go-metrics v0.5.2 + github.com/pelletier/go-toml v1.9.5 + github.com/shopspring/decimal v1.3.1 github.com/spf13/cast v1.5.1 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 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f google.golang.org/grpc v1.60.1 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 google.golang.org/protobuf v1.32.0 + gopkg.in/typ.v4 v4.3.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -120,7 +124,8 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-playground/validator/v10 v10.12.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/uuid/v5 v5.0.0 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -147,7 +152,6 @@ require ( 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-metrics v0.5.2 // indirect github.com/hashicorp/go-plugin v1.5.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -229,7 +233,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.16.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect diff --git a/go.sum b/go.sum index 2724952b0..af6c55c99 100644 --- a/go.sum +++ b/go.sum @@ -513,6 +513,7 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= @@ -546,6 +547,12 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -788,6 +795,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -929,6 +938,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= @@ -990,8 +1001,6 @@ github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3c github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= -github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1453,6 +1462,7 @@ golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1606,7 +1616,6 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= diff --git a/lib/metrics/lib.go b/lib/metrics/lib.go index bd67d1c41..e647fc3c2 100644 --- a/lib/metrics/lib.go +++ b/lib/metrics/lib.go @@ -3,8 +3,8 @@ package metrics import ( "time" - gometrics "github.com/armon/go-metrics" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" ) // This file provides a main entrypoint for logging in the v4 protocol. diff --git a/lib/metrics/util.go b/lib/metrics/util.go index cd16b4261..1929fe635 100644 --- a/lib/metrics/util.go +++ b/lib/metrics/util.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - gometrics "github.com/armon/go-metrics" "github.com/cosmos/cosmos-sdk/telemetry" + gometrics "github.com/hashicorp/go-metrics" ) // IncrCountMetricWithLabels increases a count metric from a module with the provided labels by a count of 1. diff --git a/lib/metrics/util_test.go b/lib/metrics/util_test.go index ebf2cdf88..ee58df3dc 100644 --- a/lib/metrics/util_test.go +++ b/lib/metrics/util_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - gometrics "github.com/armon/go-metrics" + gometrics "github.com/hashicorp/go-metrics" "github.com/tellor-io/layer/lib/metrics" big_testutil "github.com/tellor-io/layer/testutil/big" diff --git a/testutil/encoding/utils.go b/testutil/encoding/utils.go index a27e469bf..6e40adee1 100644 --- a/testutil/encoding/utils.go +++ b/testutil/encoding/utils.go @@ -3,12 +3,12 @@ package encoding import ( "testing" + "cosmossdk.io/x/upgrade" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module/testutil" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" - "github.com/cosmos/cosmos-sdk/x/capability" "github.com/cosmos/cosmos-sdk/x/consensus" "github.com/cosmos/cosmos-sdk/x/crisis" distr "github.com/cosmos/cosmos-sdk/x/distribution" @@ -17,14 +17,11 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov" govclient "github.com/cosmos/cosmos-sdk/x/gov/client" "github.com/cosmos/cosmos-sdk/x/params" - paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" "github.com/cosmos/cosmos-sdk/x/staking" - "github.com/cosmos/cosmos-sdk/x/upgrade" - upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" - "github.com/cosmos/ibc-go/v7/modules/apps/transfer" - ibc "github.com/cosmos/ibc-go/v7/modules/core" - ibcclientclient "github.com/cosmos/ibc-go/v7/modules/core/02-client/client" - ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" + "github.com/cosmos/ibc-go/modules/capability" + "github.com/cosmos/ibc-go/v8/modules/apps/transfer" + ibc "github.com/cosmos/ibc-go/v8/modules/core" + ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" "github.com/stretchr/testify/require" ) @@ -38,15 +35,7 @@ func GetTestEncodingCfg() testutil.TestEncodingConfig { capability.AppModuleBasic{}, staking.AppModuleBasic{}, distr.AppModuleBasic{}, - gov.NewAppModuleBasic( - []govclient.ProposalHandler{ - paramsclient.ProposalHandler, - upgradeclient.LegacyProposalHandler, - upgradeclient.LegacyCancelProposalHandler, - ibcclientclient.UpdateClientProposalHandler, - ibcclientclient.UpgradeProposalHandler, - }, - ), + gov.NewAppModuleBasic([]govclient.ProposalHandler{}), params.AppModuleBasic{}, crisis.AppModuleBasic{}, ibc.AppModuleBasic{}, diff --git a/testutil/grpc/grpc.go b/testutil/grpc/grpc.go index 330c10e9d..f0cf42cfc 100644 --- a/testutil/grpc/grpc.go +++ b/testutil/grpc/grpc.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/tellor-io/layer/mocks" + "github.com/tellor-io/layer/daemons/mocks" "google.golang.org/grpc" ) diff --git a/testutil/grpc/tcp.go b/testutil/grpc/tcp.go index ae2a27a47..60e594ae3 100644 --- a/testutil/grpc/tcp.go +++ b/testutil/grpc/tcp.go @@ -1,6 +1,6 @@ package grpc -import "github.com/tellor-io/layer/mocks" +import "github.com/tellor-io/layer/daemons/mocks" var ( TcpEndpoint = "localhost:9090" diff --git a/x/oracle/client/cli/tx_commit_submit_report.go b/x/oracle/client/cli/tx_commit_submit_report.go index b1c5c4ca6..e09171875 100644 --- a/x/oracle/client/cli/tx_commit_submit_report.go +++ b/x/oracle/client/cli/tx_commit_submit_report.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" "github.com/spf13/cobra" mediantypes "github.com/tellor-io/layer/daemons/server/types" "github.com/tellor-io/layer/lib/prices" @@ -53,7 +54,7 @@ func CmdCommitSubmitReport() *cobra.Command { if err != nil { return err } - data, _, err := clientCtx.Keyring.SignByAddress(clientCtx.GetFromAddress(), valueDecoded) + data, _, err := clientCtx.Keyring.SignByAddress(clientCtx.GetFromAddress(), valueDecoded, signing.SignMode_SIGN_MODE_DIRECT) if err != nil { return err } From e347e535d8e68f5c1ad19d432d7177ac53392bff Mon Sep 17 00:00:00 2001 From: akrem Date: Wed, 24 Jan 2024 16:51:12 -0500 Subject: [PATCH 10/10] mock stuff --- daemons/mocks/AppOptions.go | 2 +- daemons/mocks/BankKeeper.go | 194 ------------ daemons/mocks/DistrKeeper.go | 79 ----- daemons/mocks/ExchangeQueryHandler.go | 9 +- daemons/mocks/ExchangeToMarketPrices.go | 9 +- daemons/mocks/GrpcClient.go | 12 +- daemons/mocks/Makefile | 18 ++ daemons/mocks/OracleKeeper.go | 57 ---- .../mocks/PriceFeedMutableMarketConfigs.go | 26 +- daemons/mocks/QueryClient.go | 17 +- daemons/mocks/RequestHandler.go | 7 +- daemons/mocks/SlashingKeeper.go | 65 ---- daemons/mocks/StakingKeeper.go | 292 ------------------ daemons/mocks/TimeProvider.go | 2 +- daemons/mocks/readme.md | 3 + testutil/grpc/client.go | 10 + 16 files changed, 83 insertions(+), 719 deletions(-) delete mode 100644 daemons/mocks/BankKeeper.go delete mode 100644 daemons/mocks/DistrKeeper.go create mode 100644 daemons/mocks/Makefile delete mode 100644 daemons/mocks/OracleKeeper.go delete mode 100644 daemons/mocks/SlashingKeeper.go delete mode 100644 daemons/mocks/StakingKeeper.go create mode 100644 daemons/mocks/readme.md create mode 100644 testutil/grpc/client.go diff --git a/daemons/mocks/AppOptions.go b/daemons/mocks/AppOptions.go index a53be4095..535895b87 100644 --- a/daemons/mocks/AppOptions.go +++ b/daemons/mocks/AppOptions.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks diff --git a/daemons/mocks/BankKeeper.go b/daemons/mocks/BankKeeper.go deleted file mode 100644 index 0ee280a5a..000000000 --- a/daemons/mocks/BankKeeper.go +++ /dev/null @@ -1,194 +0,0 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. - -package mocks - -import ( - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - mock "github.com/stretchr/testify/mock" - - types "github.com/cosmos/cosmos-sdk/types" -) - -// BankKeeper is an autogenerated mock type for the BankKeeper type -type BankKeeper struct { - mock.Mock -} - -// BurnCoins provides a mock function with given fields: ctx, moduleName, amt -func (_m *BankKeeper) BurnCoins(ctx types.Context, moduleName string, amt types.Coins) error { - ret := _m.Called(ctx, moduleName, amt) - - if len(ret) == 0 { - panic("no return value specified for BurnCoins") - } - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, string, types.Coins) error); ok { - r0 = rf(ctx, moduleName, amt) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// GetBalance provides a mock function with given fields: ctx, addr, denom -func (_m *BankKeeper) GetBalance(ctx types.Context, addr types.AccAddress, denom string) types.Coin { - ret := _m.Called(ctx, addr, denom) - - if len(ret) == 0 { - panic("no return value specified for GetBalance") - } - - var r0 types.Coin - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, string) types.Coin); ok { - r0 = rf(ctx, addr, denom) - } else { - r0 = ret.Get(0).(types.Coin) - } - - return r0 -} - -// GetSupply provides a mock function with given fields: ctx, denom -func (_m *BankKeeper) GetSupply(ctx types.Context, denom string) types.Coin { - ret := _m.Called(ctx, denom) - - if len(ret) == 0 { - panic("no return value specified for GetSupply") - } - - var r0 types.Coin - if rf, ok := ret.Get(0).(func(types.Context, string) types.Coin); ok { - r0 = rf(ctx, denom) - } else { - r0 = ret.Get(0).(types.Coin) - } - - return r0 -} - -// HasBalance provides a mock function with given fields: ctx, addr, amt -func (_m *BankKeeper) HasBalance(ctx types.Context, addr types.AccAddress, amt types.Coin) bool { - ret := _m.Called(ctx, addr, amt) - - if len(ret) == 0 { - panic("no return value specified for HasBalance") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.Coin) bool); ok { - r0 = rf(ctx, addr, amt) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// InputOutputCoins provides a mock function with given fields: ctx, inputs, outputs -func (_m *BankKeeper) InputOutputCoins(ctx types.Context, inputs []banktypes.Input, outputs []banktypes.Output) error { - ret := _m.Called(ctx, inputs, outputs) - - if len(ret) == 0 { - panic("no return value specified for InputOutputCoins") - } - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, []banktypes.Input, []banktypes.Output) error); ok { - r0 = rf(ctx, inputs, outputs) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SendCoinsFromAccountToModule provides a mock function with given fields: ctx, senderAddr, recipientModule, amt -func (_m *BankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { - ret := _m.Called(ctx, senderAddr, recipientModule, amt) - - if len(ret) == 0 { - panic("no return value specified for SendCoinsFromAccountToModule") - } - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, string, types.Coins) error); ok { - r0 = rf(ctx, senderAddr, recipientModule, amt) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SendCoinsFromModuleToAccount provides a mock function with given fields: ctx, senderModule, recipientAddr, amt -func (_m *BankKeeper) SendCoinsFromModuleToAccount(ctx types.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { - ret := _m.Called(ctx, senderModule, recipientAddr, amt) - - if len(ret) == 0 { - panic("no return value specified for SendCoinsFromModuleToAccount") - } - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, string, types.AccAddress, types.Coins) error); ok { - r0 = rf(ctx, senderModule, recipientAddr, amt) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SendCoinsFromModuleToModule provides a mock function with given fields: ctx, senderModule, recipientModule, amt -func (_m *BankKeeper) SendCoinsFromModuleToModule(ctx types.Context, senderModule string, recipientModule string, amt types.Coins) error { - ret := _m.Called(ctx, senderModule, recipientModule, amt) - - if len(ret) == 0 { - panic("no return value specified for SendCoinsFromModuleToModule") - } - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, string, string, types.Coins) error); ok { - r0 = rf(ctx, senderModule, recipientModule, amt) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SpendableCoins provides a mock function with given fields: ctx, addr -func (_m *BankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { - ret := _m.Called(ctx, addr) - - if len(ret) == 0 { - panic("no return value specified for SpendableCoins") - } - - var r0 types.Coins - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) types.Coins); ok { - r0 = rf(ctx, addr) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.Coins) - } - } - - return r0 -} - -// NewBankKeeper creates a new instance of BankKeeper. 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 NewBankKeeper(t interface { - mock.TestingT - Cleanup(func()) -}) *BankKeeper { - mock := &BankKeeper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/daemons/mocks/DistrKeeper.go b/daemons/mocks/DistrKeeper.go deleted file mode 100644 index 6080a3ca2..000000000 --- a/daemons/mocks/DistrKeeper.go +++ /dev/null @@ -1,79 +0,0 @@ -// Code generated by mockery v2.39.1. DO NOT EDIT. - -package mocks - -import ( - distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" - mock "github.com/stretchr/testify/mock" - - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - - types "github.com/cosmos/cosmos-sdk/types" -) - -// DistrKeeper is an autogenerated mock type for the DistrKeeper type -type DistrKeeper struct { - mock.Mock -} - -// AllocateTokensToValidator provides a mock function with given fields: ctx, val, tokens -func (_m *DistrKeeper) AllocateTokensToValidator(ctx types.Context, val stakingtypes.ValidatorI, tokens types.DecCoins) { - _m.Called(ctx, val, tokens) -} - -// GetFeePool provides a mock function with given fields: ctx -func (_m *DistrKeeper) GetFeePool(ctx types.Context) distributiontypes.FeePool { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetFeePool") - } - - var r0 distributiontypes.FeePool - if rf, ok := ret.Get(0).(func(types.Context) distributiontypes.FeePool); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(distributiontypes.FeePool) - } - - return r0 -} - -// GetFeePoolCommunityCoins provides a mock function with given fields: ctx -func (_m *DistrKeeper) GetFeePoolCommunityCoins(ctx types.Context) types.DecCoins { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for GetFeePoolCommunityCoins") - } - - var r0 types.DecCoins - if rf, ok := ret.Get(0).(func(types.Context) types.DecCoins); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.DecCoins) - } - } - - return r0 -} - -// SetFeePool provides a mock function with given fields: ctx, feePool -func (_m *DistrKeeper) SetFeePool(ctx types.Context, feePool distributiontypes.FeePool) { - _m.Called(ctx, feePool) -} - -// NewDistrKeeper creates a new instance of DistrKeeper. 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 NewDistrKeeper(t interface { - mock.TestingT - Cleanup(func()) -}) *DistrKeeper { - mock := &DistrKeeper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/daemons/mocks/ExchangeQueryHandler.go b/daemons/mocks/ExchangeQueryHandler.go index 5c3b0ef4f..ea94edf9e 100644 --- a/daemons/mocks/ExchangeQueryHandler.go +++ b/daemons/mocks/ExchangeQueryHandler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks @@ -38,6 +38,11 @@ func (_m *ExchangeQueryHandler) Query(ctx context.Context, exchangeQueryDetails ret := _m.Called(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) var r0 []*types.MarketPriceTimestamp + var r1 map[uint32]error + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) ([]*types.MarketPriceTimestamp, map[uint32]error, error)); ok { + return rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) + } if rf, ok := ret.Get(0).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) []*types.MarketPriceTimestamp); ok { r0 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) } else { @@ -46,7 +51,6 @@ func (_m *ExchangeQueryHandler) Query(ctx context.Context, exchangeQueryDetails } } - var r1 map[uint32]error if rf, ok := ret.Get(1).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) map[uint32]error); ok { r1 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) } else { @@ -55,7 +59,6 @@ func (_m *ExchangeQueryHandler) Query(ctx context.Context, exchangeQueryDetails } } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, *types.ExchangeQueryDetails, *types.MutableExchangeMarketConfig, []uint32, daemonstypes.RequestHandler, map[uint32]int32) error); ok { r2 = rf(ctx, exchangeQueryDetails, exchangeConfig, marketIds, requestHandler, marketPriceExponent) } else { diff --git a/daemons/mocks/ExchangeToMarketPrices.go b/daemons/mocks/ExchangeToMarketPrices.go index adcca20ce..3065cbdb3 100644 --- a/daemons/mocks/ExchangeToMarketPrices.go +++ b/daemons/mocks/ExchangeToMarketPrices.go @@ -1,12 +1,12 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks import ( time "time" - pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" mock "github.com/stretchr/testify/mock" + pricefeedtypes "github.com/tellor-io/layer/daemons/pricefeed/types" types "github.com/tellor-io/layer/daemons/pricefeed/client/types" ) @@ -37,13 +37,16 @@ func (_m *ExchangeToMarketPrices) GetIndexPrice(marketId uint32, cutoffTime time ret := _m.Called(marketId, cutoffTime, resolver) var r0 uint64 + var r1 int + if rf, ok := ret.Get(0).(func(uint32, time.Time, pricefeedtypes.Resolver) (uint64, int)); ok { + return rf(marketId, cutoffTime, resolver) + } if rf, ok := ret.Get(0).(func(uint32, time.Time, pricefeedtypes.Resolver) uint64); ok { r0 = rf(marketId, cutoffTime, resolver) } else { r0 = ret.Get(0).(uint64) } - var r1 int if rf, ok := ret.Get(1).(func(uint32, time.Time, pricefeedtypes.Resolver) int); ok { r1 = rf(marketId, cutoffTime, resolver) } else { diff --git a/daemons/mocks/GrpcClient.go b/daemons/mocks/GrpcClient.go index eb6a39dae..965113395 100644 --- a/daemons/mocks/GrpcClient.go +++ b/daemons/mocks/GrpcClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks @@ -34,6 +34,10 @@ func (_m *GrpcClient) NewGrpcConnection(ctx context.Context, socketAddress strin ret := _m.Called(ctx, socketAddress) var r0 *grpc.ClientConn + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*grpc.ClientConn, error)); ok { + return rf(ctx, socketAddress) + } if rf, ok := ret.Get(0).(func(context.Context, string) *grpc.ClientConn); ok { r0 = rf(ctx, socketAddress) } else { @@ -42,7 +46,6 @@ func (_m *GrpcClient) NewGrpcConnection(ctx context.Context, socketAddress strin } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, socketAddress) } else { @@ -57,6 +60,10 @@ func (_m *GrpcClient) NewTcpConnection(ctx context.Context, endpoint string) (*g ret := _m.Called(ctx, endpoint) var r0 *grpc.ClientConn + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*grpc.ClientConn, error)); ok { + return rf(ctx, endpoint) + } if rf, ok := ret.Get(0).(func(context.Context, string) *grpc.ClientConn); ok { r0 = rf(ctx, endpoint) } else { @@ -65,7 +72,6 @@ func (_m *GrpcClient) NewTcpConnection(ctx context.Context, endpoint string) (*g } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, endpoint) } else { diff --git a/daemons/mocks/Makefile b/daemons/mocks/Makefile new file mode 100644 index 000000000..67fd500de --- /dev/null +++ b/daemons/mocks/Makefile @@ -0,0 +1,18 @@ +GOPATH=$(shell go env GOPATH) + +COSMOS_VERSION=$(shell go list -m all | grep "github.com/cosmos/cosmos-sdk" | awk '{print $$NF}') +COSMOS_LOG_VERSION=$(shell go list -m all | grep "cosmossdk.io/log" | awk '{print $$NF}') + +mock-clean: + @rm -f ./*.go + +mock-gen: + @go run github.com/vektra/mockery/v2 --name=AppOptions --dir=$(GOPATH)/pkg/mod/github.com/cosmos/cosmos-sdk@$(COSMOS_VERSION)/server/types --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=ExchangeQueryHandler --dir=../pricefeed/client/queryhandler --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=ExchangeToMarketPrices --dir=../pricefeed/client/types --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=GrpcClient --dir=../types --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=Logger --dir=$(GOPATH)/pkg/mod/cosmossdk.io/log@$(COSMOS_LOG_VERSION) --filename=logger.go --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=PricefeedMutableMarketConfigs --dir=../pricefeed/client/types --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=QueryClient --dir=../../testutil/grpc --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=RequestHandler --dir=../types --recursive --output=. + @go run github.com/vektra/mockery/v2 --name=TimeProvider --dir=../../lib/time --recursive --output=. \ No newline at end of file diff --git a/daemons/mocks/OracleKeeper.go b/daemons/mocks/OracleKeeper.go deleted file mode 100644 index 8f81d10a1..000000000 --- a/daemons/mocks/OracleKeeper.go +++ /dev/null @@ -1,57 +0,0 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" - oracletypes "github.com/tellor-io/layer/x/oracle/types" - - types "github.com/cosmos/cosmos-sdk/types" -) - -// OracleKeeper is an autogenerated mock type for the OracleKeeper type -type OracleKeeper struct { - mock.Mock -} - -// GetTotalTips provides a mock function with given fields: ctx -func (_m *OracleKeeper) GetTotalTips(ctx types.Context) types.Coin { - ret := _m.Called(ctx) - - var r0 types.Coin - if rf, ok := ret.Get(0).(func(types.Context) types.Coin); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(types.Coin) - } - - return r0 -} - -// GetUserTips provides a mock function with given fields: ctx, tipper -func (_m *OracleKeeper) GetUserTips(ctx types.Context, tipper types.AccAddress) oracletypes.UserTipTotal { - ret := _m.Called(ctx, tipper) - - var r0 oracletypes.UserTipTotal - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) oracletypes.UserTipTotal); ok { - r0 = rf(ctx, tipper) - } else { - r0 = ret.Get(0).(oracletypes.UserTipTotal) - } - - return r0 -} - -// NewOracleKeeper creates a new instance of OracleKeeper. 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 NewOracleKeeper(t interface { - mock.TestingT - Cleanup(func()) -}) *OracleKeeper { - mock := &OracleKeeper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/daemons/mocks/PriceFeedMutableMarketConfigs.go b/daemons/mocks/PriceFeedMutableMarketConfigs.go index f231c0c76..95994fc63 100644 --- a/daemons/mocks/PriceFeedMutableMarketConfigs.go +++ b/daemons/mocks/PriceFeedMutableMarketConfigs.go @@ -1,11 +1,10 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks import ( - types "github.com/tellor-io/layer/daemons/pricefeed/client/types" - pricestypes "github.com/tellor-io/layer/daemons/pricefeed/client/types" mock "github.com/stretchr/testify/mock" + types "github.com/tellor-io/layer/daemons/pricefeed/client/types" ) // PricefeedMutableMarketConfigs is an autogenerated mock type for the PricefeedMutableMarketConfigs type @@ -28,6 +27,10 @@ func (_m *PricefeedMutableMarketConfigs) GetExchangeMarketConfigCopy(id string) ret := _m.Called(id) var r0 *types.MutableExchangeMarketConfig + var r1 error + if rf, ok := ret.Get(0).(func(string) (*types.MutableExchangeMarketConfig, error)); ok { + return rf(id) + } if rf, ok := ret.Get(0).(func(string) *types.MutableExchangeMarketConfig); ok { r0 = rf(id) } else { @@ -36,7 +39,6 @@ func (_m *PricefeedMutableMarketConfigs) GetExchangeMarketConfigCopy(id string) } } - var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { @@ -51,6 +53,10 @@ func (_m *PricefeedMutableMarketConfigs) GetMarketConfigCopies(markets []uint32) ret := _m.Called(markets) var r0 []*types.MutableMarketConfig + var r1 error + if rf, ok := ret.Get(0).(func([]uint32) ([]*types.MutableMarketConfig, error)); ok { + return rf(markets) + } if rf, ok := ret.Get(0).(func([]uint32) []*types.MutableMarketConfig); ok { r0 = rf(markets) } else { @@ -59,7 +65,6 @@ func (_m *PricefeedMutableMarketConfigs) GetMarketConfigCopies(markets []uint32) } } - var r1 error if rf, ok := ret.Get(1).(func([]uint32) error); ok { r1 = rf(markets) } else { @@ -70,11 +75,15 @@ func (_m *PricefeedMutableMarketConfigs) GetMarketConfigCopies(markets []uint32) } // UpdateMarkets provides a mock function with given fields: marketParams -func (_m *PricefeedMutableMarketConfigs) UpdateMarkets(marketParams []pricestypes.MarketParam) (map[uint32]error, error) { +func (_m *PricefeedMutableMarketConfigs) UpdateMarkets(marketParams []types.MarketParam) (map[uint32]error, error) { ret := _m.Called(marketParams) var r0 map[uint32]error - if rf, ok := ret.Get(0).(func([]pricestypes.MarketParam) map[uint32]error); ok { + var r1 error + if rf, ok := ret.Get(0).(func([]types.MarketParam) (map[uint32]error, error)); ok { + return rf(marketParams) + } + if rf, ok := ret.Get(0).(func([]types.MarketParam) map[uint32]error); ok { r0 = rf(marketParams) } else { if ret.Get(0) != nil { @@ -82,8 +91,7 @@ func (_m *PricefeedMutableMarketConfigs) UpdateMarkets(marketParams []pricestype } } - var r1 error - if rf, ok := ret.Get(1).(func([]pricestypes.MarketParam) error); ok { + if rf, ok := ret.Get(1).(func([]types.MarketParam) error); ok { r1 = rf(marketParams) } else { r1 = ret.Error(1) diff --git a/daemons/mocks/QueryClient.go b/daemons/mocks/QueryClient.go index 82db44be0..e7a660bf2 100644 --- a/daemons/mocks/QueryClient.go +++ b/daemons/mocks/QueryClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.40.1. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks @@ -12,7 +12,7 @@ import ( types "github.com/tellor-io/layer/daemons/server/types" ) -// QueryClient is an autogenerated mock type for the PriceFeedServiceClient type +// QueryClient is an autogenerated mock type for the QueryClient type type QueryClient struct { mock.Mock } @@ -28,10 +28,6 @@ func (_m *QueryClient) UpdateMarketPrices(ctx context.Context, in *types.UpdateM _ca = append(_ca, _va...) ret := _m.Called(_ca...) - if len(ret) == 0 { - panic("no return value specified for UpdateMarketPrices") - } - var r0 *types.UpdateMarketPricesResponse var r1 error if rf, ok := ret.Get(0).(func(context.Context, *types.UpdateMarketPricesRequest, ...grpc.CallOption) (*types.UpdateMarketPricesResponse, error)); ok { @@ -54,12 +50,13 @@ func (_m *QueryClient) UpdateMarketPrices(ctx context.Context, in *types.UpdateM return r0, r1 } -// NewQueryClient creates a new instance of QueryClient. 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 NewQueryClient(t interface { +type mockConstructorTestingTNewQueryClient interface { mock.TestingT Cleanup(func()) -}) *QueryClient { +} + +// NewQueryClient creates a new instance of QueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewQueryClient(t mockConstructorTestingTNewQueryClient) *QueryClient { mock := &QueryClient{} mock.Mock.Test(t) diff --git a/daemons/mocks/RequestHandler.go b/daemons/mocks/RequestHandler.go index 6996f58d2..e1bc92e11 100644 --- a/daemons/mocks/RequestHandler.go +++ b/daemons/mocks/RequestHandler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks @@ -19,6 +19,10 @@ func (_m *RequestHandler) Get(ctx context.Context, url string) (*http.Response, ret := _m.Called(ctx, url) var r0 *http.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*http.Response, error)); ok { + return rf(ctx, url) + } if rf, ok := ret.Get(0).(func(context.Context, string) *http.Response); ok { r0 = rf(ctx, url) } else { @@ -27,7 +31,6 @@ func (_m *RequestHandler) Get(ctx context.Context, url string) (*http.Response, } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, url) } else { diff --git a/daemons/mocks/SlashingKeeper.go b/daemons/mocks/SlashingKeeper.go deleted file mode 100644 index ebee3ccf1..000000000 --- a/daemons/mocks/SlashingKeeper.go +++ /dev/null @@ -1,65 +0,0 @@ -// Code generated by mockery v2.36.0. DO NOT EDIT. - -package mocks - -import ( - time "time" - - slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" - mock "github.com/stretchr/testify/mock" - - types "github.com/cosmos/cosmos-sdk/types" -) - -// SlashingKeeper is an autogenerated mock type for the SlashingKeeper type -type SlashingKeeper struct { - mock.Mock -} - -// GetValidatorSigningInfo provides a mock function with given fields: ctx, address -func (_m *SlashingKeeper) GetValidatorSigningInfo(ctx types.Context, address types.ConsAddress) (slashingtypes.ValidatorSigningInfo, bool) { - ret := _m.Called(ctx, address) - - var r0 slashingtypes.ValidatorSigningInfo - var r1 bool - if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) (slashingtypes.ValidatorSigningInfo, bool)); ok { - return rf(ctx, address) - } - if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) slashingtypes.ValidatorSigningInfo); ok { - r0 = rf(ctx, address) - } else { - r0 = ret.Get(0).(slashingtypes.ValidatorSigningInfo) - } - - if rf, ok := ret.Get(1).(func(types.Context, types.ConsAddress) bool); ok { - r1 = rf(ctx, address) - } else { - r1 = ret.Get(1).(bool) - } - - return r0, r1 -} - -// JailUntil provides a mock function with given fields: ctx, consAddr, jailTime -func (_m *SlashingKeeper) JailUntil(ctx types.Context, consAddr types.ConsAddress, jailTime time.Time) { - _m.Called(ctx, consAddr, jailTime) -} - -// SetValidatorSigningInfo provides a mock function with given fields: ctx, address, info -func (_m *SlashingKeeper) SetValidatorSigningInfo(ctx types.Context, address types.ConsAddress, info slashingtypes.ValidatorSigningInfo) { - _m.Called(ctx, address, info) -} - -// NewSlashingKeeper creates a new instance of SlashingKeeper. 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 NewSlashingKeeper(t interface { - mock.TestingT - Cleanup(func()) -}) *SlashingKeeper { - mock := &SlashingKeeper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/daemons/mocks/StakingKeeper.go b/daemons/mocks/StakingKeeper.go deleted file mode 100644 index 926e43595..000000000 --- a/daemons/mocks/StakingKeeper.go +++ /dev/null @@ -1,292 +0,0 @@ -// Code generated by mockery v2.36.0. DO NOT EDIT. - -package mocks - -import ( - math "cosmossdk.io/math" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - mock "github.com/stretchr/testify/mock" - - types "github.com/cosmos/cosmos-sdk/types" -) - -// StakingKeeper is an autogenerated mock type for the StakingKeeper type -type StakingKeeper struct { - mock.Mock -} - -// AddValidatorTokensAndShares provides a mock function with given fields: ctx, validator, tokensToAdd -func (_m *StakingKeeper) AddValidatorTokensAndShares(ctx types.Context, validator stakingtypes.Validator, tokensToAdd math.Int) (stakingtypes.Validator, math.LegacyDec) { - ret := _m.Called(ctx, validator, tokensToAdd) - - var r0 stakingtypes.Validator - var r1 math.LegacyDec - if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.Int) (stakingtypes.Validator, math.LegacyDec)); ok { - return rf(ctx, validator, tokensToAdd) - } - if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.Int) stakingtypes.Validator); ok { - r0 = rf(ctx, validator, tokensToAdd) - } else { - r0 = ret.Get(0).(stakingtypes.Validator) - } - - if rf, ok := ret.Get(1).(func(types.Context, stakingtypes.Validator, math.Int) math.LegacyDec); ok { - r1 = rf(ctx, validator, tokensToAdd) - } else { - r1 = ret.Get(1).(math.LegacyDec) - } - - return r0, r1 -} - -// Delegate provides a mock function with given fields: ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount -func (_m *StakingKeeper) Delegate(ctx types.Context, delAddr types.AccAddress, bondAmt math.Int, tokenSrc stakingtypes.BondStatus, validator stakingtypes.Validator, subtractAccount bool) (math.LegacyDec, error) { - ret := _m.Called(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) - - var r0 math.LegacyDec - var r1 error - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, math.Int, stakingtypes.BondStatus, stakingtypes.Validator, bool) (math.LegacyDec, error)); ok { - return rf(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) - } - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, math.Int, stakingtypes.BondStatus, stakingtypes.Validator, bool) math.LegacyDec); ok { - r0 = rf(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) - } else { - r0 = ret.Get(0).(math.LegacyDec) - } - - if rf, ok := ret.Get(1).(func(types.Context, types.AccAddress, math.Int, stakingtypes.BondStatus, stakingtypes.Validator, bool) error); ok { - r1 = rf(ctx, delAddr, bondAmt, tokenSrc, validator, subtractAccount) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// DeleteValidatorByPowerIndex provides a mock function with given fields: ctx, validator -func (_m *StakingKeeper) DeleteValidatorByPowerIndex(ctx types.Context, validator stakingtypes.Validator) { - _m.Called(ctx, validator) -} - -// GetAllDelegatorDelegations provides a mock function with given fields: ctx, delegator -func (_m *StakingKeeper) GetAllDelegatorDelegations(ctx types.Context, delegator types.AccAddress) []stakingtypes.Delegation { - ret := _m.Called(ctx, delegator) - - var r0 []stakingtypes.Delegation - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) []stakingtypes.Delegation); ok { - r0 = rf(ctx, delegator) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]stakingtypes.Delegation) - } - } - - return r0 -} - -// GetDelegation provides a mock function with given fields: ctx, delAddr, valAddr -func (_m *StakingKeeper) GetDelegation(ctx types.Context, delAddr types.AccAddress, valAddr types.ValAddress) (stakingtypes.Delegation, bool) { - ret := _m.Called(ctx, delAddr, valAddr) - - var r0 stakingtypes.Delegation - var r1 bool - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.ValAddress) (stakingtypes.Delegation, bool)); ok { - return rf(ctx, delAddr, valAddr) - } - if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress, types.ValAddress) stakingtypes.Delegation); ok { - r0 = rf(ctx, delAddr, valAddr) - } else { - r0 = ret.Get(0).(stakingtypes.Delegation) - } - - if rf, ok := ret.Get(1).(func(types.Context, types.AccAddress, types.ValAddress) bool); ok { - r1 = rf(ctx, delAddr, valAddr) - } else { - r1 = ret.Get(1).(bool) - } - - return r0, r1 -} - -// GetLastTotalPower provides a mock function with given fields: ctx -func (_m *StakingKeeper) GetLastTotalPower(ctx types.Context) math.Int { - ret := _m.Called(ctx) - - var r0 math.Int - if rf, ok := ret.Get(0).(func(types.Context) math.Int); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(math.Int) - } - - return r0 -} - -// GetValidator provides a mock function with given fields: ctx, addr -func (_m *StakingKeeper) GetValidator(ctx types.Context, addr types.ValAddress) (stakingtypes.Validator, bool) { - ret := _m.Called(ctx, addr) - - var r0 stakingtypes.Validator - var r1 bool - if rf, ok := ret.Get(0).(func(types.Context, types.ValAddress) (stakingtypes.Validator, bool)); ok { - return rf(ctx, addr) - } - if rf, ok := ret.Get(0).(func(types.Context, types.ValAddress) stakingtypes.Validator); ok { - r0 = rf(ctx, addr) - } else { - r0 = ret.Get(0).(stakingtypes.Validator) - } - - if rf, ok := ret.Get(1).(func(types.Context, types.ValAddress) bool); ok { - r1 = rf(ctx, addr) - } else { - r1 = ret.Get(1).(bool) - } - - return r0, r1 -} - -// GetValidatorByConsAddr provides a mock function with given fields: ctx, consAddr -func (_m *StakingKeeper) GetValidatorByConsAddr(ctx types.Context, consAddr types.ConsAddress) (stakingtypes.Validator, bool) { - ret := _m.Called(ctx, consAddr) - - var r0 stakingtypes.Validator - var r1 bool - if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) (stakingtypes.Validator, bool)); ok { - return rf(ctx, consAddr) - } - if rf, ok := ret.Get(0).(func(types.Context, types.ConsAddress) stakingtypes.Validator); ok { - r0 = rf(ctx, consAddr) - } else { - r0 = ret.Get(0).(stakingtypes.Validator) - } - - if rf, ok := ret.Get(1).(func(types.Context, types.ConsAddress) bool); ok { - r1 = rf(ctx, consAddr) - } else { - r1 = ret.Get(1).(bool) - } - - return r0, r1 -} - -// IterateDelegatorDelegations provides a mock function with given fields: ctx, delegator, cb -func (_m *StakingKeeper) IterateDelegatorDelegations(ctx types.Context, delegator types.AccAddress, cb func(stakingtypes.Delegation) bool) { - _m.Called(ctx, delegator, cb) -} - -// Jail provides a mock function with given fields: ctx, consAddr -func (_m *StakingKeeper) Jail(ctx types.Context, consAddr types.ConsAddress) { - _m.Called(ctx, consAddr) -} - -// RemoveDelegation provides a mock function with given fields: ctx, delegation -func (_m *StakingKeeper) RemoveDelegation(ctx types.Context, delegation stakingtypes.Delegation) error { - ret := _m.Called(ctx, delegation) - - var r0 error - if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Delegation) error); ok { - r0 = rf(ctx, delegation) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// RemoveValidatorTokens provides a mock function with given fields: ctx, validator, tokensToRemove -func (_m *StakingKeeper) RemoveValidatorTokens(ctx types.Context, validator stakingtypes.Validator, tokensToRemove math.Int) stakingtypes.Validator { - ret := _m.Called(ctx, validator, tokensToRemove) - - var r0 stakingtypes.Validator - if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.Int) stakingtypes.Validator); ok { - r0 = rf(ctx, validator, tokensToRemove) - } else { - r0 = ret.Get(0).(stakingtypes.Validator) - } - - return r0 -} - -// RemoveValidatorTokensAndShares provides a mock function with given fields: ctx, validator, sharesToRemove -func (_m *StakingKeeper) RemoveValidatorTokensAndShares(ctx types.Context, validator stakingtypes.Validator, sharesToRemove math.LegacyDec) (stakingtypes.Validator, math.Int) { - ret := _m.Called(ctx, validator, sharesToRemove) - - var r0 stakingtypes.Validator - var r1 math.Int - if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.LegacyDec) (stakingtypes.Validator, math.Int)); ok { - return rf(ctx, validator, sharesToRemove) - } - if rf, ok := ret.Get(0).(func(types.Context, stakingtypes.Validator, math.LegacyDec) stakingtypes.Validator); ok { - r0 = rf(ctx, validator, sharesToRemove) - } else { - r0 = ret.Get(0).(stakingtypes.Validator) - } - - if rf, ok := ret.Get(1).(func(types.Context, stakingtypes.Validator, math.LegacyDec) math.Int); ok { - r1 = rf(ctx, validator, sharesToRemove) - } else { - r1 = ret.Get(1).(math.Int) - } - - return r0, r1 -} - -// SetDelegation provides a mock function with given fields: ctx, delegation -func (_m *StakingKeeper) SetDelegation(ctx types.Context, delegation stakingtypes.Delegation) { - _m.Called(ctx, delegation) -} - -// SetValidator provides a mock function with given fields: ctx, validator -func (_m *StakingKeeper) SetValidator(ctx types.Context, validator stakingtypes.Validator) { - _m.Called(ctx, validator) -} - -// SetValidatorByPowerIndex provides a mock function with given fields: ctx, validator -func (_m *StakingKeeper) SetValidatorByPowerIndex(ctx types.Context, validator stakingtypes.Validator) { - _m.Called(ctx, validator) -} - -// TokensFromConsensusPower provides a mock function with given fields: ctx, power -func (_m *StakingKeeper) TokensFromConsensusPower(ctx types.Context, power int64) math.Int { - ret := _m.Called(ctx, power) - - var r0 math.Int - if rf, ok := ret.Get(0).(func(types.Context, int64) math.Int); ok { - r0 = rf(ctx, power) - } else { - r0 = ret.Get(0).(math.Int) - } - - return r0 -} - -// Validator provides a mock function with given fields: ctx, address -func (_m *StakingKeeper) Validator(ctx types.Context, address types.ValAddress) stakingtypes.ValidatorI { - ret := _m.Called(ctx, address) - - var r0 stakingtypes.ValidatorI - if rf, ok := ret.Get(0).(func(types.Context, types.ValAddress) stakingtypes.ValidatorI); ok { - r0 = rf(ctx, address) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(stakingtypes.ValidatorI) - } - } - - return r0 -} - -// NewStakingKeeper creates a new instance of StakingKeeper. 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 NewStakingKeeper(t interface { - mock.TestingT - Cleanup(func()) -}) *StakingKeeper { - mock := &StakingKeeper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/daemons/mocks/TimeProvider.go b/daemons/mocks/TimeProvider.go index c5db9b121..43b872dab 100644 --- a/daemons/mocks/TimeProvider.go +++ b/daemons/mocks/TimeProvider.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.23.1. DO NOT EDIT. package mocks diff --git a/daemons/mocks/readme.md b/daemons/mocks/readme.md new file mode 100644 index 000000000..db6d5c8bf --- /dev/null +++ b/daemons/mocks/readme.md @@ -0,0 +1,3 @@ +# Mocks + +`make mock-gen` from this directory for mocks in THIS dir. diff --git a/testutil/grpc/client.go b/testutil/grpc/client.go new file mode 100644 index 000000000..226ff476c --- /dev/null +++ b/testutil/grpc/client.go @@ -0,0 +1,10 @@ +package grpc + +import ( + pricefeedtypes "github.com/tellor-io/layer/daemons/server/types" +) + +// QueryClient combines all the query clients used in testing into a single mock interface for testing convenience. +type QueryClient interface { + pricefeedtypes.PriceFeedServiceClient +}