diff --git a/app/app.go b/app/app.go index c3398d5b..088e26a5 100644 --- a/app/app.go +++ b/app/app.go @@ -152,7 +152,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) - pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()) // TODO why do other modules not need this? + pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace) // add keepers app.accountKeeper = auth.NewAccountKeeper( diff --git a/go.sum b/go.sum index b61a9570..b8895b5c 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,7 @@ github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c/go.mod h1:Fxj github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 h1:yb+E8HGzFnO0YwLS6OCBIAVWtN8KfCYoKeO9mgAmQN0= github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1/go.mod h1:IGBhkbOK1ebLqMWjtgo99zUxWHsA5IOb6N9CI8nHs0Y= github.com/cosmos/cosmos-sdk v0.37.1 h1:mz5W3Au32VIPPtrY65dheVYeVDSFfS3eSSmuIj+cXsI= +github.com/cosmos/cosmos-sdk v0.37.4 h1:1ioXxkpiS+wOgaUbROeDIyuF7hciU5nti0TSyBmV2Ok= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J96OTjeFFc= diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index 8dabeb3b..db4883b8 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -2,6 +2,7 @@ package cdp_test import ( "testing" + "time" "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" @@ -21,8 +22,8 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { tApp.InitializeFromGenesisStates( tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("xrp", 100))}), ) - // check balance ctx := tApp.NewContext(false, abci.Header{}) + // check balance tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100))) // setup cdp keeper @@ -40,17 +41,20 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { } keeper.SetParams(ctx, params) keeper.SetGlobalDebt(ctx, sdk.NewInt(0)) - // setup pricefeed - pfKeeper := tApp.GetPriceFeedKeeper() - ap := pricefeed.AssetParams{ - Assets: []pricefeed.Asset{pricefeed.Asset{AssetCode: "xrp", Description: ""}}, + pricefeedKeeper := tApp.GetPriceFeedKeeper() + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{ + MarketID: "xrp", BaseAsset: "xrp", + QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + }, } - pfKeeper.SetAssetParams(ctx, ap) - pfKeeper.SetPrice( + pricefeedKeeper.SetParams(ctx, ap) + pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, "xrp", sdk.MustNewDecFromStr("1.00"), - sdk.NewInt(10)) - pfKeeper.SetCurrentPrices(ctx) + time.Unix(9999999999, 0)) // some deterministic future date + pricefeedKeeper.SetCurrentPrices(ctx, "xrp") tApp.EndBlock(abci.RequestEndBlock{}) tApp.Commit() diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index 5bd639fa..afabd6b9 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -9,9 +9,9 @@ import ( func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, data GenesisState) { // validate denoms - check that any collaterals in the CdpParams are in the pricefeed, pricefeed needs to initgenesis before cdp collateralMap := make(map[string]int) - ap := pk.GetAssetParams(ctx) - for _, a := range ap.Assets { - collateralMap[a.AssetCode] = 1 + ap := pk.GetParams(ctx) + for _, m := range ap.Markets { + collateralMap[m.MarketID] = 1 } for _, col := range data.Params.CollateralParams { diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index aeed1a6a..7fd13c2c 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "fmt" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" @@ -105,22 +106,24 @@ func TestKeeper_ModifyCDP(t *testing.T) { keeper := tApp.GetCDPKeeper() keeper.SetParams(ctx, defaultParamsSingle()) pricefeedKeeper := tApp.GetPriceFeedKeeper() - ap := pricefeed.AssetParams{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: "xrp", Description: ""}, + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{ + MarketID: "xrp", BaseAsset: "xrp", + QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } - pricefeedKeeper.SetAssetParams(ctx, ap) + pricefeedKeeper.SetParams(ctx, ap) _, err := pricefeedKeeper.SetPrice( ctx, ownerAddr, "xrp", sdk.MustNewDecFromStr(tc.price), - sdk.NewInt(ctx.BlockHeight()+10)) + time.Unix(9999999999, 0)) // some deterministic future date if err != nil { t.Log("test context height", ctx.BlockHeight()) t.Log(err) t.Log(tc.name) } - err = pricefeedKeeper.SetCurrentPrices(ctx) + err = pricefeedKeeper.SetCurrentPrices(ctx, "xrp") if err != nil { t.Log("test context height", ctx.BlockHeight()) t.Log(err) @@ -139,9 +142,6 @@ func TestKeeper_ModifyCDP(t *testing.T) { // get new state for verification actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom) - if tc.name == "removeTooMuchCollateral" { - t.Log(actualCDP.String()) - } // check for err if tc.expectPass { require.NoError(t, err, fmt.Sprint(err)) @@ -180,17 +180,19 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { // setup pricefeed pricefeedKeeper := tApp.GetPriceFeedKeeper() - ap := pricefeed.AssetParams{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: collateral, Description: ""}, + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{ + MarketID: "xrp", BaseAsset: collateral, + QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } - pricefeedKeeper.SetAssetParams(ctx, ap) + pricefeedKeeper.SetParams(ctx, ap) pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, collateral, sdk.MustNewDecFromStr("1.00"), - i(10)) - require.NoError(t, pricefeedKeeper.SetCurrentPrices(ctx)) + time.Unix(9999999999, 0)) // some deterministic future date + pricefeedKeeper.SetCurrentPrices(ctx, collateral) // Create CDP keeper.SetParams(ctx, defaultParamsSingle()) @@ -200,8 +202,8 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, collateral, sdk.MustNewDecFromStr("0.90"), - i(10)) - require.NoError(t, pricefeedKeeper.SetCurrentPrices(ctx)) + time.Unix(9999999999, 0)) // some deterministic future date + pricefeedKeeper.SetCurrentPrices(ctx, collateral) // Seize entire CDP err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5)) diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go index fe41bed2..41562789 100644 --- a/x/cdp/types/expected_keepers.go +++ b/x/cdp/types/expected_keepers.go @@ -1,6 +1,8 @@ package types import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" pftypes "github.com/kava-labs/kava/x/pricefeed/types" ) @@ -14,9 +16,9 @@ type BankKeeper interface { type PricefeedKeeper interface { GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice - GetAssetParams(sdk.Context) pftypes.AssetParams + GetParams(sdk.Context) pftypes.Params // These are used for testing TODO replace mockApp with keeper in tests to remove these - SetAssetParams(sdk.Context, pftypes.AssetParams) - SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, sdk.Int) (pftypes.PostedPrice, sdk.Error) - SetCurrentPrices(sdk.Context) sdk.Error + SetParams(sdk.Context, pftypes.Params) + SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, sdk.Error) + SetCurrentPrices(sdk.Context, string) sdk.Error } diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 96596e46..04ac4654 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -1,8 +1,6 @@ package types -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) +import sdk "github.com/cosmos/cosmos-sdk/types" // GenesisState is the state that must be provided at genesis. // TODO What is globaldebt and is is separate from the global debt limit in CdpParams @@ -18,9 +16,8 @@ type GenesisState struct { // TODO make this empty, load test values independent func DefaultGenesisState() GenesisState { return GenesisState{ - Params: DefaultParams(), - GlobalDebt: sdk.ZeroInt(), - CDPs: CDPs{}, + Params: DefaultParams(), + CDPs: CDPs{}, } } diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index d69b5547..23fdd974 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -2,10 +2,12 @@ package keeper import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/mock" "github.com/stretchr/testify/require" + tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/liquidator/types" @@ -19,8 +21,8 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { _, addrs := mock.GeneratePrivKeyAddressPairs(1) pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis()) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999)) - k.pricefeedKeeper.SetCurrentPrices(ctx) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis()) dp := defaultParams() k.liquidatorKeeper.SetParams(ctx, dp) @@ -28,8 +30,8 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { k.cdpKeeper.ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), i(999999999)) - k.pricefeedKeeper.SetCurrentPrices(ctx) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") // Run test function auctionID, err := k.liquidatorKeeper.SeizeAndStartCollateralAuction(ctx, addrs[0], "btc") @@ -100,16 +102,16 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis()) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999)) - k.pricefeedKeeper.SetCurrentPrices(ctx) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") k.bankKeeper.AddCoins(ctx, addrs[0], cs(c("btc", 100))) cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis()) k.liquidatorKeeper.SetParams(ctx, defaultParams()) k.cdpKeeper.ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), i(999999999)) - k.pricefeedKeeper.SetCurrentPrices(ctx) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") // Run test function err := k.liquidatorKeeper.partialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000)) diff --git a/x/liquidator/keeper/test_common.go b/x/liquidator/keeper/test_common.go index 1f5550d4..ab2d0a6b 100644 --- a/x/liquidator/keeper/test_common.go +++ b/x/liquidator/keeper/test_common.go @@ -1,6 +1,8 @@ package keeper import ( + "time" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" @@ -9,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/params" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" + tmtime "github.com/tendermint/tendermint/types/time" dbm "github.com/tendermint/tm-db" "github.com/kava-labs/kava/x/auction" @@ -75,7 +78,7 @@ func setupTestKeepers() (sdk.Context, keepers) { bank.DefaultCodespace, blacklistedAddrs, ) - pricefeedKeeper := pricefeed.NewKeeper(cdc, keyPriceFeed, paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) + pricefeedKeeper := pricefeed.NewKeeper(cdc, keyPriceFeed, paramsKeeper.Subspace(pricefeed.DefaultParamspace), pricefeed.DefaultCodespace) cdpKeeper := cdp.NewKeeper( cdc, keyCDP, @@ -150,20 +153,18 @@ func cdpDefaultGenesis() cdp.GenesisState { } func pricefeedGenesis() pricefeed.GenesisState { - ap := pricefeed.AssetParams{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: "btc", Description: "a description"}, - }, + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}}, } return pricefeed.GenesisState{ - AssetParams: ap, - OracleParams: pricefeed.DefaultOracleParams(), + Params: ap, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ - AssetCode: "btc", - OracleAddress: "", + MarketID: "btc", + OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("8000.00"), - Expiry: sdk.NewInt(999999999), + Expiry: tmtime.Now().Add(1 * time.Hour), }, }, } diff --git a/x/pricefeed/abci.go b/x/pricefeed/abci.go new file mode 100644 index 00000000..ac0ca068 --- /dev/null +++ b/x/pricefeed/abci.go @@ -0,0 +1,28 @@ +package pricefeed + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// EndBlocker updates the current pricefeed +func EndBlocker(ctx sdk.Context, k Keeper) { + // Update the current price of each asset. + for _, a := range k.GetMarketParams(ctx) { + if a.Active { + err := k.SetCurrentPrices(ctx, a.MarketID) + if err != nil { + // In the event of failure, emit an event. + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeNoValidPrices, + sdk.NewAttribute(AttributeKeyPriceUpdateFailed, fmt.Sprintf("%s", a.MarketID)), + ), + ) + continue + } + } + } + return +} diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index 61be278c..01a51ea7 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -2,26 +2,36 @@ // autogenerated code using github.com/rigelrozanski/multitool // aliases generated for the following subdirectories: // ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/ +// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/keeper/ package pricefeed import ( + "github.com/kava-labs/kava/x/pricefeed/keeper" "github.com/kava-labs/kava/x/pricefeed/types" ) const ( - DefaultCodespace = types.DefaultCodespace - CodeEmptyInput = types.CodeEmptyInput - CodeExpired = types.CodeExpired - CodeInvalidPrice = types.CodeInvalidPrice - CodeInvalidAsset = types.CodeInvalidAsset - CodeInvalidOracle = types.CodeInvalidOracle - ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - TypeMsgPostPrice = types.TypeMsgPostPrice - QueryCurrentPrice = types.QueryCurrentPrice - QueryRawPrices = types.QueryRawPrices - QueryAssets = types.QueryAssets + DefaultCodespace = types.DefaultCodespace + CodeEmptyInput = types.CodeEmptyInput + CodeExpired = types.CodeExpired + CodeInvalidPrice = types.CodeInvalidPrice + CodeInvalidAsset = types.CodeInvalidAsset + CodeInvalidOracle = types.CodeInvalidOracle + EventTypeNoValidPrices = types.EventTypeNoValidPrices + AttributeKeyPriceUpdateFailed = types.AttributeKeyPriceUpdateFailed + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + RawPriceFeedPrefix = types.RawPriceFeedPrefix + CurrentPricePrefix = types.CurrentPricePrefix + AssetPrefix = types.AssetPrefix + OraclePrefix = types.OraclePrefix + TypeMsgPostPrice = types.TypeMsgPostPrice + QueryCurrentPrice = types.QueryCurrentPrice + QueryRawPrices = types.QueryRawPrices + QueryAssets = types.QueryAssets ) var ( @@ -37,28 +47,29 @@ var ( ValidateGenesis = types.ValidateGenesis NewMsgPostPrice = types.NewMsgPostPrice ParamKeyTable = types.ParamKeyTable - NewAssetParams = types.NewAssetParams - DefaultAssetParams = types.DefaultAssetParams - NewOracleParams = types.NewOracleParams - DefaultOracleParams = types.DefaultOracleParams + NewParams = types.NewParams + DefaultParams = types.DefaultParams + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - ParamStoreKeyOracles = types.ParamStoreKeyOracles - ParamStoreKeyAssets = types.ParamStoreKeyAssets + ModuleCdc = types.ModuleCdc + KeyMarkets = types.KeyMarkets ) type ( - GenesisState = types.GenesisState - MsgPostPrice = types.MsgPostPrice - AssetParams = types.AssetParams - OracleParams = types.OracleParams - ParamSubspace = types.ParamSubspace - QueryRawPricesResp = types.QueryRawPricesResp - QueryAssetsResp = types.QueryAssetsResp - Asset = types.Asset + Market = types.Market + Markets = types.Markets Oracle = types.Oracle + Oracles = types.Oracles CurrentPrice = types.CurrentPrice PostedPrice = types.PostedPrice SortDecs = types.SortDecs + GenesisState = types.GenesisState + MsgPostPrice = types.MsgPostPrice + Params = types.Params + ParamSubspace = types.ParamSubspace + QueryRawPricesResp = types.QueryRawPricesResp + QueryAssetsResp = types.QueryAssetsResp + Keeper = keeper.Keeper ) diff --git a/x/pricefeed/client/cli/tx.go b/x/pricefeed/client/cli/tx.go index 8c505856..199ad2a0 100644 --- a/x/pricefeed/client/cli/tx.go +++ b/x/pricefeed/client/cli/tx.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "time" "github.com/spf13/cobra" @@ -12,6 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/kava-labs/kava/x/pricefeed/types" + tmtime "github.com/tendermint/tendermint/types/time" ) // GetTxCmd returns the transaction commands for this module @@ -47,11 +49,13 @@ func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command { if err != nil { return err } - expiry, ok := sdk.NewIntFromString(args[2]) + expiryInt, ok := sdk.NewIntFromString(args[2]) if !ok { fmt.Printf("invalid expiry - %s \n", args[2]) return nil } + expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) + msg := types.NewMsgPostPrice(cliCtx.GetFromAddress(), args[0], price, expiry) err = msg.ValidateBasic() if err != nil { diff --git a/x/pricefeed/client/rest/rest.go b/x/pricefeed/client/rest/rest.go index ea6990a6..988d8ea5 100644 --- a/x/pricefeed/client/rest/rest.go +++ b/x/pricefeed/client/rest/rest.go @@ -3,6 +3,7 @@ package rest import ( "fmt" "net/http" + "time" "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/gorilla/mux" "github.com/kava-labs/kava/x/pricefeed/types" + tmtime "github.com/tendermint/tendermint/types/time" ) const ( @@ -57,11 +59,12 @@ func postPriceHandler(cliCtx context.CLIContext) http.HandlerFunc { return } - expiry, ok := sdk.NewIntFromString(req.Expiry) + expiryInt, ok := sdk.NewIntFromString(req.Expiry) if !ok { rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry") return } + expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) // create the message msg := types.NewMsgPostPrice(addr, req.AssetCode, price, expiry) diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index ed19d3e6..3bf16251 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -8,24 +8,21 @@ import ( func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { // Set the assets and oracles from params - keeper.SetAssetParams(ctx, data.AssetParams) - keeper.SetOracleParams(ctx, data.OracleParams) + keeper.SetParams(ctx, data.Params) // Iterate through the posted prices and set them in the store for _, pp := range data.PostedPrices { - addr, err := sdk.AccAddressFromBech32(pp.OracleAddress) - if err != nil { - panic(err) - } - _, err = keeper.SetPrice(ctx, addr, pp.AssetCode, pp.Price, pp.Expiry) + _, err := keeper.SetPrice(ctx, pp.OracleAddress, pp.MarketID, pp.Price, pp.Expiry) if err != nil { panic(err) } } // Set the current price (if any) based on what's now in the store - if err := keeper.SetCurrentPrices(ctx); err != nil { - panic(err) + for _, a := range data.Params.Markets { + if a.Active { + _ = keeper.SetCurrentPrices(ctx, a.MarketID) + } } } @@ -33,18 +30,16 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { // Get the params for assets and oracles - assetParams := keeper.GetAssetParams(ctx) - oracleParams := keeper.GetOracleParams(ctx) + params := keeper.GetParams(ctx) var postedPrices []PostedPrice - for _, asset := range keeper.GetAssets(ctx) { - pp := keeper.GetRawPrices(ctx, asset.AssetCode) + for _, asset := range keeper.GetMarketParams(ctx) { + pp := keeper.GetRawPrices(ctx, asset.MarketID) postedPrices = append(postedPrices, pp...) } return GenesisState{ - AssetParams: assetParams, - OracleParams: oracleParams, + Params: params, PostedPrices: postedPrices, } } diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go index 1b54d4e4..704839f9 100644 --- a/x/pricefeed/handler.go +++ b/x/pricefeed/handler.go @@ -29,22 +29,10 @@ func HandleMsgPostPrice( msg MsgPostPrice) sdk.Result { // TODO cleanup message validation and errors - err := k.ValidatePostPrice(ctx, msg) + _, err := k.GetOracle(ctx, msg.AssetCode, msg.From) if err != nil { - return err.Result() + return ErrInvalidOracle(k.Codespace()).Result() } k.SetPrice(ctx, msg.From, msg.AssetCode, msg.Price, msg.Expiry) return sdk.Result{} } - -// EndBlocker updates the current pricefeed -func EndBlocker(ctx sdk.Context, k Keeper) { - // TODO val_state_change.go is relevant if we want to rotate the oracle set - - // Running in the end blocker ensures that prices will update at most once per block, - // which seems preferable to having state storage values change in response to multiple transactions - // which occur during a block - //TODO use an iterator and update the prices for all assets in the store - k.SetCurrentPrices(ctx) - return -} diff --git a/x/pricefeed/keeper.go b/x/pricefeed/keeper.go deleted file mode 100644 index 092cb168..00000000 --- a/x/pricefeed/keeper.go +++ /dev/null @@ -1,280 +0,0 @@ -package pricefeed - -import ( - "sort" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// TODO refactor constants to app.go -const ( - // QuerierRoute is the querier route for gov - QuerierRoute = ModuleName - - // Parameter store default namestore - DefaultParamspace = ModuleName - - // Store prefix for the raw pricefeed of an asset - RawPriceFeedPrefix = StoreKey + ":raw:" - - // Store prefix for the current price of an asset - CurrentPricePrefix = StoreKey + ":currentprice:" - - // Store Prefix for the assets in the pricefeed system - AssetPrefix = StoreKey + ":assets" - - // OraclePrefix store prefix for the oracle accounts - OraclePrefix = StoreKey + ":oracles" -) - -// Keeper struct for pricefeed module -type Keeper struct { - // The reference to the Paramstore to get and set pricefeed specific params - paramSpace ParamSubspace - // The keys used to access the stores from Context - storeKey sdk.StoreKey - // Codec for binary encoding/decoding - cdc *codec.Codec - // Reserved codespace - codespace sdk.CodespaceType -} - -// NewKeeper returns a new keeper for the pricefeed module. It handles: -// - adding oracles -// - adding/removing assets from the pricefeed -func NewKeeper( - cdc *codec.Codec, storeKey sdk.StoreKey, paramSpace ParamSubspace, codespace sdk.CodespaceType, -) Keeper { - return Keeper{ - paramSpace: paramSpace, - storeKey: storeKey, - cdc: cdc, - codespace: codespace, - } -} - -// // AddOracle adds an Oracle to the store -// func (k Keeper) AddOracle(ctx sdk.Context, address string) { - -// oracles := k.GetOracles(ctx) -// oracles = append(oracles, Oracle{OracleAddress: address}) -// store := ctx.KVStore(k.storeKey) -// store.Set( -// []byte(OraclePrefix), k.cdc.MustMarshalBinaryBare(oracles), -// ) -// } - -// // AddAsset adds an asset to the store -// func (k Keeper) AddAsset( -// ctx sdk.Context, -// assetCode string, -// desc string, -// ) { -// assets := k.GetAssets(ctx) -// assets = append(assets, Asset{AssetCode: assetCode, Description: desc}) -// store := ctx.KVStore(k.storeKey) -// store.Set( -// []byte(AssetPrefix), k.cdc.MustMarshalBinaryBare(assets), -// ) -// } - -func (k Keeper) SetAssetParams(ctx sdk.Context, ap AssetParams) { - k.paramSpace.Set(ctx, ParamStoreKeyAssets, &ap) -} - -func (k Keeper) SetOracleParams(ctx sdk.Context, op OracleParams) { - k.paramSpace.Set(ctx, ParamStoreKeyOracles, &op) -} - -// SetPrice updates the posted price for a specific oracle -func (k Keeper) SetPrice( - ctx sdk.Context, - oracle sdk.AccAddress, - assetCode string, - price sdk.Dec, - expiry sdk.Int) (PostedPrice, sdk.Error) { - // If the expiry is less than or equal to the current blockheight, we consider the price valid - if expiry.GTE(sdk.NewInt(ctx.BlockHeight())) { - store := ctx.KVStore(k.storeKey) - prices := k.GetRawPrices(ctx, assetCode) - var index int - found := false - for i := range prices { - if prices[i].OracleAddress == oracle.String() { - index = i - found = true - break - } - } - // set the price for that particular oracle - if found { - prices[index] = PostedPrice{AssetCode: assetCode, OracleAddress: oracle.String(), Price: price, Expiry: expiry} - } else { - prices = append(prices, PostedPrice{ - assetCode, oracle.String(), price, expiry, - }) - index = len(prices) - 1 - } - - store.Set( - []byte(RawPriceFeedPrefix+assetCode), k.cdc.MustMarshalBinaryBare(prices), - ) - return prices[index], nil - } - return PostedPrice{}, ErrExpired(k.codespace) - -} - -// SetCurrentPrices updates the price of an asset to the meadian of all valid oracle inputs -func (k Keeper) SetCurrentPrices(ctx sdk.Context) sdk.Error { - assets := k.GetAssets(ctx) - for _, v := range assets { - assetCode := v.AssetCode - prices := k.GetRawPrices(ctx, assetCode) - var notExpiredPrices []CurrentPrice - // filter out expired prices - for _, v := range prices { - if v.Expiry.GTE(sdk.NewInt(ctx.BlockHeight())) { - notExpiredPrices = append(notExpiredPrices, CurrentPrice{ - AssetCode: v.AssetCode, - Price: v.Price, - Expiry: v.Expiry, - }) - } - } - l := len(notExpiredPrices) - var medianPrice sdk.Dec - var expiry sdk.Int - // TODO make threshold for acceptance (ie. require 51% of oracles to have posted valid prices - if l == 0 { - // Error if there are no valid prices in the raw pricefeed - return ErrNoValidPrice(k.codespace) - } else if l == 1 { - // Return immediately if there's only one price - medianPrice = notExpiredPrices[0].Price - expiry = notExpiredPrices[0].Expiry - } else { - // sort the prices - sort.Slice(notExpiredPrices, func(i, j int) bool { - return notExpiredPrices[i].Price.LT(notExpiredPrices[j].Price) - }) - // If there's an even number of prices - if l%2 == 0 { - // TODO make sure this is safe. - // Since it's a price and not a balance, division with precision loss is OK. - price1 := notExpiredPrices[l/2-1].Price - price2 := notExpiredPrices[l/2].Price - sum := price1.Add(price2) - divsor, _ := sdk.NewDecFromStr("2") - medianPrice = sum.Quo(divsor) - // TODO Check if safe, makes sense - // Takes the average of the two expiries rounded down to the nearest Int. - expiry = notExpiredPrices[l/2-1].Expiry.Add(notExpiredPrices[l/2].Expiry).Quo(sdk.NewInt(2)) - } else { - // integer division, so we'll get an integer back, rounded down - medianPrice = notExpiredPrices[l/2].Price - expiry = notExpiredPrices[l/2].Expiry - } - } - - store := ctx.KVStore(k.storeKey) - currentPrice := CurrentPrice{ - AssetCode: assetCode, - Price: medianPrice, - Expiry: expiry, - } - store.Set( - []byte(CurrentPricePrefix+assetCode), k.cdc.MustMarshalBinaryBare(currentPrice), - ) - } - - return nil -} - -func (k Keeper) GetOracleParams(ctx sdk.Context) OracleParams { - var op OracleParams - k.paramSpace.Get(ctx, ParamStoreKeyOracles, &op) - return op -} - -// GetOracles returns the oracles in the pricefeed store -func (k Keeper) GetOracles(ctx sdk.Context) []Oracle { - var op OracleParams - k.paramSpace.Get(ctx, ParamStoreKeyOracles, &op) - return op.Oracles -} - -func (k Keeper) GetAssetParams(ctx sdk.Context) AssetParams { - var ap AssetParams - k.paramSpace.Get(ctx, ParamStoreKeyAssets, &ap) - return ap -} - -// GetAssets returns the assets in the pricefeed store -func (k Keeper) GetAssets(ctx sdk.Context) []Asset { - var ap AssetParams - k.paramSpace.Get(ctx, ParamStoreKeyAssets, &ap) - return ap.Assets -} - -// GetAsset returns the asset if it is in the pricefeed system -func (k Keeper) GetAsset(ctx sdk.Context, assetCode string) (Asset, bool) { - assets := k.GetAssets(ctx) - - for i := range assets { - if assets[i].AssetCode == assetCode { - return assets[i], true - } - } - return Asset{}, false - -} - -// GetOracle returns the oracle address as a string if it is in the pricefeed store -func (k Keeper) GetOracle(ctx sdk.Context, oracle string) (Oracle, bool) { - oracles := k.GetOracles(ctx) - - for i := range oracles { - if oracles[i].OracleAddress == oracle { - return oracles[i], true - } - } - return Oracle{}, false - -} - -// GetCurrentPrice fetches the current median price of all oracles for a specific asset -func (k Keeper) GetCurrentPrice(ctx sdk.Context, assetCode string) CurrentPrice { - store := ctx.KVStore(k.storeKey) - bz := store.Get([]byte(CurrentPricePrefix + assetCode)) - // TODO panic or return error if not found - var price CurrentPrice - k.cdc.MustUnmarshalBinaryBare(bz, &price) - return price -} - -// GetRawPrices fetches the set of all prices posted by oracles for an asset -func (k Keeper) GetRawPrices(ctx sdk.Context, assetCode string) []PostedPrice { - store := ctx.KVStore(k.storeKey) - bz := store.Get([]byte(RawPriceFeedPrefix + assetCode)) - var prices []PostedPrice - k.cdc.MustUnmarshalBinaryBare(bz, &prices) - return prices -} - -// ValidatePostPrice makes sure the person posting the price is an oracle -func (k Keeper) ValidatePostPrice(ctx sdk.Context, msg MsgPostPrice) sdk.Error { - // TODO implement this - - _, assetFound := k.GetAsset(ctx, msg.AssetCode) - if !assetFound { - return ErrInvalidAsset(k.codespace) - } - _, oracleFound := k.GetOracle(ctx, msg.From.String()) - if !oracleFound { - return ErrInvalidOracle(k.codespace) - } - - return nil -} diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go new file mode 100644 index 00000000..b181f11b --- /dev/null +++ b/x/pricefeed/keeper/keeper.go @@ -0,0 +1,168 @@ +package keeper + +import ( + "sort" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" + + "github.com/kava-labs/kava/x/pricefeed/types" +) + +// Keeper struct for pricefeed module +type Keeper struct { + // The keys used to access the stores from Context + storeKey sdk.StoreKey + // Codec for binary encoding/decoding + cdc *codec.Codec + // The reference to the Paramstore to get and set pricefeed specific params + paramstore params.Subspace + // Reserved codespace + codespace sdk.CodespaceType +} + +// NewKeeper returns a new keeper for the pricefeed module. It handles: +// - adding oracles +// - adding/removing assets from the pricefeed +func NewKeeper( + cdc *codec.Codec, storeKey sdk.StoreKey, paramstore params.Subspace, codespace sdk.CodespaceType, +) Keeper { + return Keeper{ + paramstore: paramstore.WithKeyTable(types.ParamKeyTable()), + storeKey: storeKey, + cdc: cdc, + codespace: codespace, + } +} + +// SetPrice updates the posted price for a specific oracle +func (k Keeper) SetPrice( + ctx sdk.Context, + oracle sdk.AccAddress, + marketID string, + price sdk.Dec, + expiry time.Time) (types.PostedPrice, sdk.Error) { + // If the expiry is less than or equal to the current blockheight, we consider the price valid + if expiry.After(ctx.BlockTime()) { + store := ctx.KVStore(k.storeKey) + prices := k.GetRawPrices(ctx, marketID) + var index int + found := false + for i := range prices { + if prices[i].OracleAddress.Equals(oracle) { + index = i + found = true + break + } + } + // set the price for that particular oracle + if found { + prices[index] = types.PostedPrice{ + MarketID: marketID, OracleAddress: oracle, + Price: price, Expiry: expiry} + } else { + prices = append(prices, types.PostedPrice{ + MarketID: marketID, OracleAddress: oracle, + Price: price, Expiry: expiry}) + index = len(prices) - 1 + } + + store.Set( + []byte(types.RawPriceFeedPrefix+marketID), k.cdc.MustMarshalBinaryBare(prices), + ) + return prices[index], nil + } + return types.PostedPrice{}, types.ErrExpired(k.codespace) + +} + +// SetCurrentPrices updates the price of an asset to the meadian of all valid oracle inputs +func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { + _, ok := k.GetMarket(ctx, marketID) + if !ok { + return types.ErrInvalidAsset(k.codespace) + } + prices := k.GetRawPrices(ctx, marketID) + var notExpiredPrices []types.CurrentPrice + // filter out expired prices + for _, v := range prices { + if v.Expiry.After(ctx.BlockTime()) { + notExpiredPrices = append(notExpiredPrices, types.CurrentPrice{ + MarketID: v.MarketID, + Price: v.Price, + }) + } + } + medianPrice, err := k.CalculateMedianPrice(ctx, notExpiredPrices) + if err != nil { + return err + } + + store := ctx.KVStore(k.storeKey) + currentPrice := types.CurrentPrice{ + MarketID: marketID, + Price: medianPrice, + } + store.Set( + []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(currentPrice), + ) + + return nil +} + +// CalculateMedianPrice calculates the median prices for the input prices. +func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) (sdk.Dec, sdk.Error) { + l := len(prices) + + if l == 0 { + // Error if there are no valid prices in the raw pricefeed + return sdk.Dec{}, types.ErrNoValidPrice(k.codespace) + } else if l == 1 { + // Return immediately if there's only one price + return prices[0].Price, nil + } else { + // sort the prices + sort.Slice(prices, func(i, j int) bool { + return prices[i].Price.LT(prices[j].Price) + }) + // for even numbers of prices, the median is calculated as the mean of the two middle prices + if l%2 == 0 { + median := k.calculateMeanPrice(ctx, prices[l/2-1:l/2+1]) + return median, nil + } + // for odd numbers of prices, return the middle element + return prices[l/2].Price, nil + } +} + +func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec { + sum := prices[0].Price.Add(prices[1].Price) + mean := sum.Quo(sdk.NewDec(2)) + return mean +} + +// GetCurrentPrice fetches the current median price of all oracles for a specific asset +func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) types.CurrentPrice { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(types.CurrentPricePrefix + marketID)) + // TODO panic or return error if not found + var price types.CurrentPrice + k.cdc.MustUnmarshalBinaryBare(bz, &price) + return price +} + +// GetRawPrices fetches the set of all prices posted by oracles for an asset +func (k Keeper) GetRawPrices(ctx sdk.Context, marketID string) []types.PostedPrice { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(types.RawPriceFeedPrefix + marketID)) + var prices []types.PostedPrice + k.cdc.MustUnmarshalBinaryBare(bz, &prices) + return prices +} + +// Codespace return the codespace for the keeper +func (k Keeper) Codespace() sdk.CodespaceType { + return k.codespace +} diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go new file mode 100644 index 00000000..105062bb --- /dev/null +++ b/x/pricefeed/keeper/keeper_test.go @@ -0,0 +1,141 @@ +package keeper + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/x/pricefeed/types" +) + +// TestKeeper_SetGetMarket tests adding markets to the pricefeed, getting markets from the store +func TestKeeper_SetGetMarket(t *testing.T) { + helper := getMockApp(t, 0, types.GenesisState{}, nil) + header := abci.Header{ + Height: helper.mApp.LastBlockHeight() + 1, + Time: tmtime.Now()} + helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + ctx := helper.mApp.BaseApp.NewContext(false, header) + + mp := types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, mp) + markets := helper.keeper.GetMarketParams(ctx) + require.Equal(t, len(markets), 1) + require.Equal(t, markets[0].MarketID, "tstusd") + + _, found := helper.keeper.GetMarket(ctx, "tstusd") + require.Equal(t, found, true) + + mp = types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Market{MarketID: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, mp) + markets = helper.keeper.GetMarketParams(ctx) + require.Equal(t, len(markets), 2) + require.Equal(t, markets[0].MarketID, "tstusd") + require.Equal(t, markets[1].MarketID, "tst2usd") + + _, found = helper.keeper.GetMarket(ctx, "nan") + require.Equal(t, found, false) +} + +// TestKeeper_GetSetPrice Test Posting the price by an oracle +func TestKeeper_GetSetPrice(t *testing.T) { + helper := getMockApp(t, 2, types.GenesisState{}, nil) + header := abci.Header{ + Height: helper.mApp.LastBlockHeight() + 1, + Time: tmtime.Now()} + helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + ctx := helper.mApp.BaseApp.NewContext(false, header) + mp := types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, mp) + // Set price by oracle 1 + _, err := helper.keeper.SetPrice( + ctx, helper.addrs[0], "tstusd", + sdk.MustNewDecFromStr("0.33"), + header.Time.Add(1*time.Hour)) + require.NoError(t, err) + // Get raw prices + rawPrices := helper.keeper.GetRawPrices(ctx, "tstusd") + require.Equal(t, len(rawPrices), 1) + require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.33")), true) + // Set price by oracle 2 + _, err = helper.keeper.SetPrice( + ctx, helper.addrs[1], "tstusd", + sdk.MustNewDecFromStr("0.35"), + header.Time.Add(time.Hour*1)) + require.NoError(t, err) + + rawPrices = helper.keeper.GetRawPrices(ctx, "tstusd") + require.Equal(t, len(rawPrices), 2) + require.Equal(t, rawPrices[1].Price.Equal(sdk.MustNewDecFromStr("0.35")), true) + + // Update Price by Oracle 1 + _, err = helper.keeper.SetPrice( + ctx, helper.addrs[0], "tstusd", + sdk.MustNewDecFromStr("0.37"), + header.Time.Add(time.Hour*1)) + require.NoError(t, err) + rawPrices = helper.keeper.GetRawPrices(ctx, "tstusd") + require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.37")), true) +} + +// TestKeeper_GetSetCurrentPrice Test Setting the median price of an Asset +func TestKeeper_GetSetCurrentPrice(t *testing.T) { + helper := getMockApp(t, 4, types.GenesisState{}, nil) + header := abci.Header{ + Height: helper.mApp.LastBlockHeight() + 1, + Time: tmtime.Now()} + helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + ctx := helper.mApp.BaseApp.NewContext(false, header) + mp := types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, mp) + helper.keeper.SetPrice( + ctx, helper.addrs[0], "tstusd", + sdk.MustNewDecFromStr("0.33"), + header.Time.Add(time.Hour*1)) + helper.keeper.SetPrice( + ctx, helper.addrs[1], "tstusd", + sdk.MustNewDecFromStr("0.35"), + header.Time.Add(time.Hour*1)) + helper.keeper.SetPrice( + ctx, helper.addrs[2], "tstusd", + sdk.MustNewDecFromStr("0.34"), + header.Time.Add(time.Hour*1)) + // Set current price + err := helper.keeper.SetCurrentPrices(ctx, "tstusd") + require.NoError(t, err) + // Get Current price + price := helper.keeper.GetCurrentPrice(ctx, "tstusd") + require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true) + + // Even number of oracles + helper.keeper.SetPrice( + ctx, helper.addrs[3], "tstusd", + sdk.MustNewDecFromStr("0.36"), + header.Time.Add(time.Hour*1)) + err = helper.keeper.SetCurrentPrices(ctx, "tstusd") + require.NoError(t, err) + price = helper.keeper.GetCurrentPrice(ctx, "tstusd") + require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true) + +} diff --git a/x/pricefeed/keeper/params.go b/x/pricefeed/keeper/params.go new file mode 100644 index 00000000..aa64cca1 --- /dev/null +++ b/x/pricefeed/keeper/params.go @@ -0,0 +1,64 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/pricefeed/types" +) + +// GetParams gets params from the store +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + return types.NewParams(k.GetMarketParams(ctx)) +} + +// SetParams updates params in the store +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramstore.SetParamSet(ctx, ¶ms) +} + +// GetMarketParams get asset params from store +func (k Keeper) GetMarketParams(ctx sdk.Context) types.Markets { + var markets types.Markets + k.paramstore.Get(ctx, types.KeyMarkets, &markets) + return markets +} + +// GetOracles returns the oracles in the pricefeed store +func (k Keeper) GetOracles(ctx sdk.Context, marketID string) (types.Oracles, error) { + + for _, m := range k.GetMarketParams(ctx) { + if marketID == m.MarketID { + return m.Oracles, nil + } + } + return types.Oracles{}, fmt.Errorf("asset %s not found", marketID) +} + +// GetOracle returns the oracle from the store or an error if not found +func (k Keeper) GetOracle(ctx sdk.Context, marketID string, address sdk.AccAddress) (types.Oracle, error) { + oracles, err := k.GetOracles(ctx, marketID) + if err != nil { + return types.Oracle{}, fmt.Errorf("asset %s not found", marketID) + } + for _, o := range oracles { + if address.Equals(o.Address) { + return o, nil + } + } + return types.Oracle{}, fmt.Errorf("oracle %s not found for asset %s", address, marketID) +} + +// GetMarket returns the market if it is in the pricefeed system +func (k Keeper) GetMarket(ctx sdk.Context, marketID string) (types.Market, bool) { + markets := k.GetMarketParams(ctx) + + for i := range markets { + if markets[i].MarketID == marketID { + return markets[i], true + } + } + return types.Market{}, false + +} diff --git a/x/pricefeed/querier.go b/x/pricefeed/keeper/querier.go similarity index 77% rename from x/pricefeed/querier.go rename to x/pricefeed/keeper/querier.go index ecc7936c..a862b776 100644 --- a/x/pricefeed/querier.go +++ b/x/pricefeed/keeper/querier.go @@ -1,21 +1,23 @@ -package pricefeed +package keeper import ( "github.com/cosmos/cosmos-sdk/codec" - abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/pricefeed/types" ) // NewQuerier is the module level router for state queries func NewQuerier(keeper Keeper) sdk.Querier { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { switch path[0] { - case QueryCurrentPrice: + case types.QueryCurrentPrice: return queryCurrentPrice(ctx, path[1:], req, keeper) - case QueryRawPrices: + case types.QueryRawPrices: return queryRawPrices(ctx, path[1:], req, keeper) - case QueryAssets: + case types.QueryAssets: return queryAssets(ctx, req, keeper) default: return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint") @@ -25,12 +27,12 @@ func NewQuerier(keeper Keeper) sdk.Querier { } func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - assetCode := path[0] - _, found := keeper.GetAsset(ctx, assetCode) + marketID := path[0] + _, found := keeper.GetMarket(ctx, marketID) if !found { return []byte{}, sdk.ErrUnknownRequest("asset not found") } - currentPrice := keeper.GetCurrentPrice(ctx, assetCode) + currentPrice := keeper.GetCurrentPrice(ctx, marketID) bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice) if err2 != nil { @@ -41,13 +43,13 @@ func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, ke } func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var priceList QueryRawPricesResp - assetCode := path[0] - _, found := keeper.GetAsset(ctx, assetCode) + var priceList types.QueryRawPricesResp + marketID := path[0] + _, found := keeper.GetMarket(ctx, marketID) if !found { return []byte{}, sdk.ErrUnknownRequest("asset not found") } - rawPrices := keeper.GetRawPrices(ctx, assetCode) + rawPrices := keeper.GetRawPrices(ctx, marketID) for _, price := range rawPrices { priceList = append(priceList, price.String()) } @@ -60,8 +62,8 @@ func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keepe } func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var assetList QueryAssetsResp - assets := keeper.GetAssets(ctx) + var assetList types.QueryAssetsResp + assets := keeper.GetMarketParams(ctx) for _, asset := range assets { assetList = append(assetList, asset.String()) } @@ -71,4 +73,4 @@ func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []b } return bz, nil -} \ No newline at end of file +} diff --git a/x/pricefeed/test_common.go b/x/pricefeed/keeper/test_common.go similarity index 55% rename from x/pricefeed/test_common.go rename to x/pricefeed/keeper/test_common.go index 7808d01b..1f42c5ba 100644 --- a/x/pricefeed/test_common.go +++ b/x/pricefeed/keeper/test_common.go @@ -1,4 +1,4 @@ -package pricefeed +package keeper import ( "testing" @@ -7,8 +7,9 @@ import ( authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" "github.com/cosmos/cosmos-sdk/x/mock" "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" + + "github.com/kava-labs/kava/x/pricefeed/types" ) type testHelper struct { @@ -19,16 +20,12 @@ type testHelper struct { privKeys []crypto.PrivKey } -func getMockApp(t *testing.T, numGenAccs int, genState GenesisState, genAccs []authexported.Account) testHelper { +func getMockApp(t *testing.T, numGenAccs int, genState types.GenesisState, genAccs []authexported.Account) testHelper { mApp := mock.NewApp() - RegisterCodec(mApp.Cdc) - keyPricefeed := sdk.NewKVStoreKey(StoreKey) + types.RegisterCodec(mApp.Cdc) + keyPricefeed := sdk.NewKVStoreKey(types.StoreKey) pk := mApp.ParamsKeeper - keeper := NewKeeper(mApp.Cdc, keyPricefeed, pk.Subspace(DefaultParamspace).WithKeyTable(ParamKeyTable()), DefaultCodespace) - - // Register routes - mApp.Router().AddRoute(RouterKey, NewHandler(keeper)) - mApp.SetEndBlocker(getEndBlocker(keeper)) + keeper := NewKeeper(mApp.Cdc, keyPricefeed, pk.Subspace(types.DefaultParamspace), types.DefaultCodespace) require.NoError(t, mApp.CompleteSetup(keyPricefeed)) @@ -47,11 +44,3 @@ func getMockApp(t *testing.T, numGenAccs int, genState GenesisState, genAccs []a mock.SetGenesis(mApp, genAccs) return testHelper{mApp, keeper, addrs, pubKeys, privKeys} } - -// gov and staking endblocker -func getEndBlocker(keeper Keeper) sdk.EndBlocker { - return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - EndBlocker(ctx, keeper) - return abci.ResponseEndBlock{} - } -} diff --git a/x/pricefeed/keeper_test.go b/x/pricefeed/keeper_test.go deleted file mode 100644 index 9ee5f857..00000000 --- a/x/pricefeed/keeper_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package pricefeed - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" -) - -// TestKeeper_SetGetAsset tests adding assets to the pricefeed, getting assets from the store -func TestKeeper_SetGetAsset(t *testing.T) { - helper := getMockApp(t, 0, GenesisState{}, nil) - header := abci.Header{Height: helper.mApp.LastBlockHeight() + 1} - helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) - ctx := helper.mApp.BaseApp.NewContext(false, abci.Header{}) - ap := AssetParams{ - Assets: []Asset{Asset{AssetCode: "tst", Description: "the future of finance"}}, - } - helper.keeper.SetAssetParams(ctx, ap) - assets := helper.keeper.GetAssets(ctx) - require.Equal(t, len(assets), 1) - require.Equal(t, assets[0].AssetCode, "tst") - - _, found := helper.keeper.GetAsset(ctx, "tst") - require.Equal(t, found, true) - - ap = AssetParams{ - Assets: []Asset{ - Asset{AssetCode: "tst", Description: "the future of finance"}, - Asset{AssetCode: "tst2", Description: "the future of finance"}}, - } - helper.keeper.SetAssetParams(ctx, ap) - assets = helper.keeper.GetAssets(ctx) - require.Equal(t, len(assets), 2) - require.Equal(t, assets[0].AssetCode, "tst") - require.Equal(t, assets[1].AssetCode, "tst2") - - _, found = helper.keeper.GetAsset(ctx, "nan") - require.Equal(t, found, false) -} - -// TestKeeper_GetSetPrice Test Posting the price by an oracle -func TestKeeper_GetSetPrice(t *testing.T) { - helper := getMockApp(t, 2, GenesisState{}, nil) - header := abci.Header{Height: helper.mApp.LastBlockHeight() + 1} - helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) - ctx := helper.mApp.BaseApp.NewContext(false, abci.Header{}) - ap := AssetParams{ - Assets: []Asset{Asset{AssetCode: "tst", Description: "the future of finance"}}, - } - helper.keeper.SetAssetParams(ctx, ap) - // Set price by oracle 1 - _, err := helper.keeper.SetPrice( - ctx, helper.addrs[0], "tst", - sdk.MustNewDecFromStr("0.33"), - sdk.NewInt(10)) - require.NoError(t, err) - // Get raw prices - rawPrices := helper.keeper.GetRawPrices(ctx, "tst") - require.Equal(t, len(rawPrices), 1) - require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.33")), true) - // Set price by oracle 2 - _, err = helper.keeper.SetPrice( - ctx, helper.addrs[1], "tst", - sdk.MustNewDecFromStr("0.35"), - sdk.NewInt(10)) - require.NoError(t, err) - - rawPrices = helper.keeper.GetRawPrices(ctx, "tst") - require.Equal(t, len(rawPrices), 2) - require.Equal(t, rawPrices[1].Price.Equal(sdk.MustNewDecFromStr("0.35")), true) - - // Update Price by Oracle 1 - _, err = helper.keeper.SetPrice( - ctx, helper.addrs[0], "tst", - sdk.MustNewDecFromStr("0.37"), - sdk.NewInt(10)) - require.NoError(t, err) - rawPrices = helper.keeper.GetRawPrices(ctx, "tst") - require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.37")), true) -} - -// TestKeeper_GetSetCurrentPrice Test Setting the median price of an Asset -func TestKeeper_GetSetCurrentPrice(t *testing.T) { - helper := getMockApp(t, 4, GenesisState{}, nil) - header := abci.Header{Height: helper.mApp.LastBlockHeight() + 1} - helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) - ctx := helper.mApp.BaseApp.NewContext(false, abci.Header{}) - // Odd number of oracles - ap := AssetParams{ - Assets: []Asset{Asset{AssetCode: "tst", Description: "the future of finance"}}, - } - helper.keeper.SetAssetParams(ctx, ap) - helper.keeper.SetPrice( - ctx, helper.addrs[0], "tst", - sdk.MustNewDecFromStr("0.33"), - sdk.NewInt(10)) - helper.keeper.SetPrice( - ctx, helper.addrs[1], "tst", - sdk.MustNewDecFromStr("0.35"), - sdk.NewInt(10)) - helper.keeper.SetPrice( - ctx, helper.addrs[2], "tst", - sdk.MustNewDecFromStr("0.34"), - sdk.NewInt(10)) - // Set current price - err := helper.keeper.SetCurrentPrices(ctx) - require.NoError(t, err) - // Get Current price - price := helper.keeper.GetCurrentPrice(ctx, "tst") - require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true) - - // Even number of oracles - helper.keeper.SetPrice( - ctx, helper.addrs[3], "tst", - sdk.MustNewDecFromStr("0.36"), - sdk.NewInt(10)) - err = helper.keeper.SetCurrentPrices(ctx) - require.NoError(t, err) - price = helper.keeper.GetCurrentPrice(ctx, "tst") - require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true) - -} diff --git a/x/pricefeed/types/asset.go b/x/pricefeed/types/asset.go new file mode 100644 index 00000000..bc19b532 --- /dev/null +++ b/x/pricefeed/types/asset.go @@ -0,0 +1,98 @@ +package types + +import ( + "fmt" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Market struct that represents an asset in the pricefeed +type Market struct { + MarketID string `json:"market_id" yaml:"market_id"` + BaseAsset string `json:"base_asset" yaml:"base_asset"` + QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` + Oracles Oracles `json:"oracles" yaml:"oracles"` + Active bool `json:"active" yaml:"active"` +} + +// String implement fmt.Stringer +func (a Market) String() string { + return fmt.Sprintf(`Asset: + Market ID: %s + Base Asset: %s + Quote Asset: %s + Oracles: %s + Active: %t`, + a.MarketID, a.BaseAsset, a.QuoteAsset, a.Oracles, a.Active) +} + +// Markets array type for oracle +type Markets []Market + +// String implements fmt.Stringer +func (ms Markets) String() string { + out := "Markets:\n" + for _, m := range ms { + out += fmt.Sprintf("%s\n", m.String()) + } + return strings.TrimSpace(out) +} + +// Oracle struct that documents which address an oracle is using +type Oracle struct { + Address sdk.AccAddress `json:"address" yaml:"address"` +} + +// String implements fmt.Stringer +func (o Oracle) String() string { + return fmt.Sprintf(`Address: %s`, o.Address) +} + +// Oracles array type for oracle +type Oracles []Oracle + +// String implements fmt.Stringer +func (os Oracles) String() string { + out := "Oracles:\n" + for _, o := range os { + out += fmt.Sprintf("%s\n", o.String()) + } + return strings.TrimSpace(out) +} + +// CurrentPrice struct that contains the metadata of a current price for a particular asset in the pricefeed module. +type CurrentPrice struct { + MarketID string `json:"market_id" yaml:"market_id"` + Price sdk.Dec `json:"price" yaml:"price"` +} + +// PostedPrice struct represented a price for an asset posted by a specific oracle +type PostedPrice struct { + MarketID string `json:"market_id" yaml:"market_id"` + OracleAddress sdk.AccAddress `json:"oracle_address" yaml:"oracle_address"` + Price sdk.Dec `json:"price" yaml:"price"` + Expiry time.Time `json:"expiry" yaml:"expiry"` +} + +// implement fmt.Stringer +func (cp CurrentPrice) String() string { + return strings.TrimSpace(fmt.Sprintf(`Market ID: %s +Price: %s`, cp.MarketID, cp.Price)) +} + +// implement fmt.Stringer +func (pp PostedPrice) String() string { + return strings.TrimSpace(fmt.Sprintf(`Market ID: %s +Oracle Address: %s +Price: %s +Expiry: %s`, pp.MarketID, pp.OracleAddress, pp.Price, pp.Expiry)) +} + +// SortDecs provides the interface needed to sort sdk.Dec slices +type SortDecs []sdk.Dec + +func (a SortDecs) Len() int { return len(a) } +func (a SortDecs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortDecs) Less(i, j int) bool { return a[i].LT(a[j]) } diff --git a/x/pricefeed/types/events.go b/x/pricefeed/types/events.go new file mode 100644 index 00000000..0bf5fa51 --- /dev/null +++ b/x/pricefeed/types/events.go @@ -0,0 +1,8 @@ +package types + +// Pricefeed module event types +const ( + EventTypeNoValidPrices = "no_valid_prices" + + AttributeKeyPriceUpdateFailed = "price_update_failed" +) diff --git a/x/pricefeed/types/genesis.go b/x/pricefeed/types/genesis.go index d798e6d8..775143c1 100644 --- a/x/pricefeed/types/genesis.go +++ b/x/pricefeed/types/genesis.go @@ -2,21 +2,18 @@ package types import ( "bytes" - "fmt" ) // GenesisState - pricefeed state that must be provided at genesis type GenesisState struct { - AssetParams AssetParams `json:"asset_params" yaml:"asset_params"` - OracleParams OracleParams `json:"oracle_params" yaml:"oracle_params"` + Params Params `json:"asset_params" yaml:"asset_params"` PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"` } // NewGenesisState creates a new genesis state for the pricefeed module -func NewGenesisState(ap AssetParams, op OracleParams, pp []PostedPrice) GenesisState { +func NewGenesisState(p Params, pp []PostedPrice) GenesisState { return GenesisState{ - AssetParams: ap, - OracleParams: op, + Params: p, PostedPrices: pp, } } @@ -24,8 +21,7 @@ func NewGenesisState(ap AssetParams, op OracleParams, pp []PostedPrice) GenesisS // DefaultGenesisState defines default GenesisState for pricefeed func DefaultGenesisState() GenesisState { return NewGenesisState( - DefaultAssetParams(), - DefaultOracleParams(), + DefaultParams(), []PostedPrice{}, ) } @@ -45,19 +41,9 @@ func (data GenesisState) IsEmpty() bool { // ValidateGenesis performs basic validation of genesis data returning an // error for any failed validation criteria. func ValidateGenesis(data GenesisState) error { - // iterate over assets and verify them - for _, asset := range data.AssetParams.Assets { - if asset.AssetCode == "" { - return fmt.Errorf("invalid asset: %s. missing asset code", asset.String()) - } - } - // iterate over oracles and verify them - for _, oracle := range data.OracleParams.Oracles { - if oracle.OracleAddress == "" { - return fmt.Errorf("invalid oracle: %s. missing oracle address", oracle.String()) - } + if err := data.Params.Validate(); err != nil { + return err } - return nil } diff --git a/x/pricefeed/types/key.go b/x/pricefeed/types/key.go index e39d2d56..4a20c25a 100644 --- a/x/pricefeed/types/key.go +++ b/x/pricefeed/types/key.go @@ -9,4 +9,22 @@ const ( // RouterKey Top level router key RouterKey = ModuleName + + // QuerierRoute is the querier route for gov + QuerierRoute = ModuleName + + // DefaultParamspace default namestore + DefaultParamspace = ModuleName + + // RawPriceFeedPrefix prefix for the raw pricefeed of an asset + RawPriceFeedPrefix = StoreKey + ":raw:" + + // CurrentPricePrefix prefix for the current price of an asset + CurrentPricePrefix = StoreKey + ":currentprice:" + + // AssetPrefix Prefix for the assets in the pricefeed system + AssetPrefix = StoreKey + ":assets" + + // OraclePrefix store prefix for the oracle accounts + OraclePrefix = StoreKey + ":oracles" ) diff --git a/x/pricefeed/types/msgs.go b/x/pricefeed/types/msgs.go index 3aa23ece..eed227bc 100644 --- a/x/pricefeed/types/msgs.go +++ b/x/pricefeed/types/msgs.go @@ -1,6 +1,8 @@ package types import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -15,7 +17,7 @@ type MsgPostPrice struct { From sdk.AccAddress // client that sent in this address AssetCode string // asset code used by exchanges/api Price sdk.Dec // price in decimal (max precision 18) - Expiry sdk.Int // block height + Expiry time.Time // expiry time } // NewMsgPostPrice creates a new post price msg @@ -23,7 +25,7 @@ func NewMsgPostPrice( from sdk.AccAddress, assetCode string, price sdk.Dec, - expiry sdk.Int) MsgPostPrice { + expiry time.Time) MsgPostPrice { return MsgPostPrice{ From: from, AssetCode: assetCode, @@ -60,9 +62,6 @@ func (msg MsgPostPrice) ValidateBasic() sdk.Error { if msg.Price.LT(sdk.ZeroDec()) { return sdk.ErrInternal("invalid (negative) price") } - if msg.Expiry.LT(sdk.ZeroInt()) { - return sdk.ErrInternal("invalid (negative) expiry") - } // TODO check coin denoms return nil } diff --git a/x/pricefeed/types/msgs_test.go b/x/pricefeed/types/msgs_test.go index 73857091..fe139969 100644 --- a/x/pricefeed/types/msgs_test.go +++ b/x/pricefeed/types/msgs_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + tmtime "github.com/tendermint/tendermint/types/time" ) func TestMsgPlaceBid_ValidateBasic(t *testing.T) { @@ -13,8 +14,7 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) { // OracleAddress: addr.String(), // }} price, _ := sdk.NewDecFromStr("0.3005") - expiry, _ := sdk.NewIntFromString("10") - negativeExpiry, _ := sdk.NewIntFromString("-3") + expiry := tmtime.Now() negativePrice, _ := sdk.NewDecFromStr("-3.05") tests := []struct { @@ -26,7 +26,6 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) { {"emptyAddr", MsgPostPrice{sdk.AccAddress{}, "xrp", price, expiry}, false}, {"emptyAsset", MsgPostPrice{addr, "", price, expiry}, false}, {"negativePrice", MsgPostPrice{addr, "xrp", negativePrice, expiry}, false}, - {"negativeExpiry", MsgPostPrice{addr, "xrp", price, negativeExpiry}, false}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go index a3d86768..fdef14d3 100644 --- a/x/pricefeed/types/params.go +++ b/x/pricefeed/types/params.go @@ -5,77 +5,51 @@ import ( "strings" sdk "github.com/cosmos/cosmos-sdk/types" - params "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/cosmos/cosmos-sdk/x/params" ) -// Parameter store key var ( - // ParamStoreKeyOracles Param store key for oracles - ParamStoreKeyOracles = []byte("oracles") - // ParamStoreKeyAssets Param store key for assets - ParamStoreKeyAssets = []byte("assets") + // KeyMarkets store key for markets + KeyMarkets = []byte("Markets") ) // ParamKeyTable Key declaration for parameters func ParamKeyTable() params.KeyTable { - return params.NewKeyTable( - ParamStoreKeyOracles, OracleParams{}, - ParamStoreKeyAssets, AssetParams{}, - ) + return params.NewKeyTable().RegisterParamSet(&Params{}) } -// AssetParams params for assets. Can be altered via governance -type AssetParams struct { - Assets []Asset `json:"assets,omitempty" yaml:"assets,omitempty"` // Array containing the assets supported by the pricefeed +// Params params for pricefeed. Can be altered via governance +type Params struct { + Markets Markets `json:"markets" yaml:"markets"` // Array containing the markets supported by the pricefeed } -// NewAssetParams creates a new AssetParams object -func NewAssetParams(assets []Asset) AssetParams { - return AssetParams{ - Assets: assets, +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs +// pairs of pricefeed module's parameters. +func (p Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyMarkets, Value: &p.Markets}, } } -// DefaultAssetParams default params for assets -func DefaultAssetParams() AssetParams { - return NewAssetParams([]Asset{}) -} - -// implements fmt.stringer -func (ap AssetParams) String() string { - var assetListString []string - for _, asset := range ap.Assets { - assetListString = append(assetListString, asset.String()) - } - return strings.TrimSpace(fmt.Sprintf(`Asset Params: - Assets: %s\`, strings.Join(assetListString, ", "))) -} - -// OracleParams params for assets. Can be altered via governance -type OracleParams struct { - Oracles []Oracle `json:"oracles,omitempty" yaml:"oracles,omitempty"` // Array containing the oracles supported by the pricefeed -} - -// NewOracleParams creates a new OracleParams object -func NewOracleParams(oracles []Oracle) OracleParams { - return OracleParams{ - Oracles: oracles, +// NewParams creates a new AssetParams object +func NewParams(markets Markets) Params { + return Params{ + Markets: markets, } } -// DefaultOracleParams default params for assets -func DefaultOracleParams() OracleParams { - return NewOracleParams([]Oracle{}) +// DefaultParams default params for pricefeed +func DefaultParams() Params { + return NewParams(Markets{}) } -// implements fmt.stringer -func (op OracleParams) String() string { - var oracleListString []string - for _, oracle := range op.Oracles { - oracleListString = append(oracleListString, oracle.String()) +// String implements fmt.stringer +func (p Params) String() string { + out := "Params:\n" + for _, a := range p.Markets { + out += a.String() } - return strings.TrimSpace(fmt.Sprintf(`Oracle Params: - Oracles: %s\`, strings.Join(oracleListString, ", "))) + return strings.TrimSpace(out) } // ParamSubspace defines the expected Subspace interface for parameters @@ -83,3 +57,14 @@ type ParamSubspace interface { Get(ctx sdk.Context, key []byte, ptr interface{}) Set(ctx sdk.Context, key []byte, param interface{}) } + +// Validate ensure that params have valid values +func (p Params) Validate() error { + // iterate over assets and verify them + for _, asset := range p.Markets { + if asset.MarketID == "" { + return fmt.Errorf("invalid market: %s. missing market ID", asset.String()) + } + } + return nil +} diff --git a/x/pricefeed/types/types.go b/x/pricefeed/types/types.go deleted file mode 100644 index e07d9b55..00000000 --- a/x/pricefeed/types/types.go +++ /dev/null @@ -1,67 +0,0 @@ -package types - -import ( - "fmt" - "strings" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Asset struct that represents an asset in the pricefeed -type Asset struct { - AssetCode string `json:"asset_code"` - Description string `json:"description"` -} - -// implement fmt.Stringer -func (a Asset) String() string { - return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s -Description: %s`, a.AssetCode, a.Description)) -} - -// Oracle struct that documents which address an oracle is using -type Oracle struct { - OracleAddress string `json:"oracle_address"` -} - -// implement fmt.Stringer -func (o Oracle) String() string { - return strings.TrimSpace(fmt.Sprintf(`OracleAddress: %s`, o.OracleAddress)) -} - -// CurrentPrice struct that contains the metadata of a current price for a particular asset in the pricefeed module. -type CurrentPrice struct { - AssetCode string `json:"asset_code"` - Price sdk.Dec `json:"price"` - Expiry sdk.Int `json:"expiry"` -} - -// PostedPrice struct represented a price for an asset posted by a specific oracle -type PostedPrice struct { - AssetCode string `json:"asset_code"` - OracleAddress string `json:"oracle_address"` - Price sdk.Dec `json:"price"` - Expiry sdk.Int `json:"expiry"` -} - -// implement fmt.Stringer -func (cp CurrentPrice) String() string { - return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s -Price: %s -Expiry: %s`, cp.AssetCode, cp.Price, cp.Expiry)) -} - -// implement fmt.Stringer -func (pp PostedPrice) String() string { - return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s -OracleAddress: %s -Price: %s -Expiry: %s`, pp.AssetCode, pp.OracleAddress, pp.Price, pp.Expiry)) -} - -// SortDecs provides the interface needed to sort sdk.Dec slices -type SortDecs []sdk.Dec - -func (a SortDecs) Len() int { return len(a) } -func (a SortDecs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortDecs) Less(i, j int) bool { return a[i].LT(a[j]) }