From d5da161dd81187d3fc173eff55e3594b2977c111 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 27 Nov 2019 09:45:59 -0500 Subject: [PATCH] feat: draft params for pricefeed --- go.sum | 1 + x/pricefeed/abci.go | 20 ++ x/pricefeed/alias.go | 53 +++-- x/pricefeed/client/cli/tx.go | 6 +- x/pricefeed/client/rest/rest.go | 5 +- x/pricefeed/genesis.go | 24 +- x/pricefeed/handler.go | 16 +- x/pricefeed/keeper.go | 280 ------------------------ x/pricefeed/keeper/keeper.go | 160 ++++++++++++++ x/pricefeed/keeper/keeper_test.go | 141 ++++++++++++ x/pricefeed/keeper/params.go | 64 ++++++ x/pricefeed/{ => keeper}/querier.go | 20 +- x/pricefeed/{ => keeper}/test_common.go | 25 +-- x/pricefeed/keeper_test.go | 124 ----------- x/pricefeed/types/asset.go | 98 +++++++++ x/pricefeed/types/genesis.go | 26 +-- x/pricefeed/types/key.go | 18 ++ x/pricefeed/types/msgs.go | 9 +- x/pricefeed/types/msgs_test.go | 5 +- x/pricefeed/types/params.go | 89 ++++---- x/pricefeed/types/types.go | 67 ------ 21 files changed, 620 insertions(+), 631 deletions(-) create mode 100644 x/pricefeed/abci.go delete mode 100644 x/pricefeed/keeper.go create mode 100644 x/pricefeed/keeper/keeper.go create mode 100644 x/pricefeed/keeper/keeper_test.go create mode 100644 x/pricefeed/keeper/params.go rename x/pricefeed/{ => keeper}/querier.go (87%) rename x/pricefeed/{ => keeper}/test_common.go (55%) delete mode 100644 x/pricefeed/keeper_test.go create mode 100644 x/pricefeed/types/asset.go delete mode 100644 x/pricefeed/types/types.go 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/pricefeed/abci.go b/x/pricefeed/abci.go new file mode 100644 index 00000000..67a0cb0b --- /dev/null +++ b/x/pricefeed/abci.go @@ -0,0 +1,20 @@ +package pricefeed + +import ( + 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.GetAssetParams(ctx) { + if a.Active { + err := k.SetCurrentPrices(ctx, a.AssetCode) + if err != nil { + // TODO emit an event that price failed to update + continue + } + } + } + return +} diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index 61be278c..bbe6e654 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -2,26 +2,34 @@ // 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 + 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 +45,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 + KeyAssets = types.KeyAssets ) type ( GenesisState = types.GenesisState MsgPostPrice = types.MsgPostPrice - AssetParams = types.AssetParams - OracleParams = types.OracleParams + Params = types.Params ParamSubspace = types.ParamSubspace QueryRawPricesResp = types.QueryRawPricesResp QueryAssetsResp = types.QueryAssetsResp Asset = types.Asset + Assets = types.Assets Oracle = types.Oracle + Oracles = types.Oracles CurrentPrice = types.CurrentPrice PostedPrice = types.PostedPrice SortDecs = types.SortDecs + 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 2dafc2b3..87cb45fc 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -4,29 +4,25 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) - // InitGenesis sets distribution information for genesis. 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.AssetCode, 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.Assets { + if a.Active { + _ = keeper.SetCurrentPrices(ctx, a.AssetCode) + } } } @@ -34,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) { + for _, asset := range keeper.GetAssetParams(ctx) { pp := keeper.GetRawPrices(ctx, asset.AssetCode) 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 40473812..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( - storeKey sdk.StoreKey, cdc *codec.Codec, 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..a5fcfa0d --- /dev/null +++ b/x/pricefeed/keeper/keeper.go @@ -0,0 +1,160 @@ +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( + storeKey sdk.StoreKey, cdc *codec.Codec, 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, + assetCode 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, assetCode) + 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{ + AssetCode: assetCode, OracleAddress: oracle, + Price: price, Expiry: expiry} + } else { + prices = append(prices, types.PostedPrice{ + AssetCode: assetCode, OracleAddress: oracle, + Price: price, Expiry: expiry}) + index = len(prices) - 1 + } + + store.Set( + []byte(types.RawPriceFeedPrefix+assetCode), 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, assetCode string) sdk.Error { + _, ok := k.GetAsset(ctx, assetCode) + if !ok { + return types.ErrInvalidAsset(k.codespace) + } + prices := k.GetRawPrices(ctx, assetCode) + var notExpiredPrices []types.CurrentPrice + // filter out expired prices + for _, v := range prices { + if v.Expiry.After(ctx.BlockTime()) { + notExpiredPrices = append(notExpiredPrices, types.CurrentPrice{ + AssetCode: v.AssetCode, + Price: v.Price, + }) + } + } + l := len(notExpiredPrices) + var medianPrice sdk.Dec + // 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 types.ErrNoValidPrice(k.codespace) + } else if l == 1 { + // Return immediately if there's only one price + medianPrice = notExpiredPrices[0].Price + } 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) + } else { + // integer division, so we'll get an integer back, rounded down + medianPrice = notExpiredPrices[l/2].Price + } + } + + store := ctx.KVStore(k.storeKey) + currentPrice := types.CurrentPrice{ + AssetCode: assetCode, + Price: medianPrice, + } + store.Set( + []byte(types.CurrentPricePrefix+assetCode), k.cdc.MustMarshalBinaryBare(currentPrice), + ) + + return nil +} + +// GetCurrentPrice fetches the current median price of all oracles for a specific asset +func (k Keeper) GetCurrentPrice(ctx sdk.Context, assetCode string) types.CurrentPrice { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(types.CurrentPricePrefix + assetCode)) + // 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, assetCode string) []types.PostedPrice { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(types.RawPriceFeedPrefix + assetCode)) + var prices []types.PostedPrice + k.cdc.MustUnmarshalBinaryBare(bz, &prices) + return prices +} + +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..1c93cb9d --- /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_SetGetAsset tests adding assets to the pricefeed, getting assets from the store +func TestKeeper_SetGetAsset(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) + + ap := types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + assets := helper.keeper.GetAssetParams(ctx) + require.Equal(t, len(assets), 1) + require.Equal(t, assets[0].AssetCode, "tstusd") + + _, found := helper.keeper.GetAsset(ctx, "tstusd") + require.Equal(t, found, true) + + ap = types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Asset{AssetCode: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + assets = helper.keeper.GetAssetParams(ctx) + require.Equal(t, len(assets), 2) + require.Equal(t, assets[0].AssetCode, "tstusd") + require.Equal(t, assets[1].AssetCode, "tst2usd") + + _, 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, 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) + ap := types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + // 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) + ap := types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + 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..576c9398 --- /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.GetAssetParams(ctx)) +} + +// SetParams updates params in the store +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramstore.SetParamSet(ctx, ¶ms) +} + +// GetAssetParams get asset params from store +func (k Keeper) GetAssetParams(ctx sdk.Context) types.Assets { + var assets types.Assets + k.paramstore.Get(ctx, types.KeyAssets, &assets) + return assets +} + +// GetOracles returns the oracles in the pricefeed store +func (k Keeper) GetOracles(ctx sdk.Context, assetCode string) (types.Oracles, error) { + + for _, a := range k.GetAssetParams(ctx) { + if assetCode == a.AssetCode { + return a.Oracles, nil + } + } + return types.Oracles{}, fmt.Errorf("asset %s not found", assetCode) +} + +// GetOracle returns the oracle from the store or an error if not found +func (k Keeper) GetOracle(ctx sdk.Context, assetCode string, address sdk.AccAddress) (types.Oracle, error) { + oracles, err := k.GetOracles(ctx, assetCode) + if err != nil { + return types.Oracle{}, fmt.Errorf("asset %s not found", assetCode) + } + 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, assetCode) +} + +// GetAsset returns the asset if it is in the pricefeed system +func (k Keeper) GetAsset(ctx sdk.Context, assetCode string) (types.Asset, bool) { + assets := k.GetAssetParams(ctx) + + for i := range assets { + if assets[i].AssetCode == assetCode { + return assets[i], true + } + } + return types.Asset{}, false + +} diff --git a/x/pricefeed/querier.go b/x/pricefeed/keeper/querier.go similarity index 87% rename from x/pricefeed/querier.go rename to x/pricefeed/keeper/querier.go index ecc7936c..245fa754 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") @@ -41,7 +43,7 @@ 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 + var priceList types.QueryRawPricesResp assetCode := path[0] _, found := keeper.GetAsset(ctx, assetCode) if !found { @@ -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.GetAssetParams(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 5fea3df1..2f0e8206 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(keyPricefeed, mApp.Cdc, pk.Subspace(DefaultParamspace).WithKeyTable(ParamKeyTable()), DefaultCodespace) - - // Register routes - mApp.Router().AddRoute(RouterKey, NewHandler(keeper)) - mApp.SetEndBlocker(getEndBlocker(keeper)) + keeper := NewKeeper(keyPricefeed, mApp.Cdc, 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..83ba1c0b --- /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" +) + +// Asset struct that represents an asset in the pricefeed +type Asset struct { + AssetCode string `json:"asset_code" yaml:"asset_code"` + 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"` +} + +// implement fmt.Stringer +func (a Asset) String() string { + return fmt.Sprintf(`Asset: + Asset Code: %s + Base Asset: %s + Quote Asset: %s + Oracles: %s + Active: %t`, + a.AssetCode, a.BaseAsset, a.QuoteAsset, a.Oracles, a.Active) +} + +// Assets array type for oracle +type Assets []Asset + +// String implements fmt.Stringer +func (as Assets) String() string { + out := "Assets:\n" + for _, a := range as { + out += fmt.Sprintf("%s\n", a.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 { + AssetCode string `json:"asset_code" yaml:"asset_code"` + Price sdk.Dec `json:"price" yaml:"price"` +} + +// PostedPrice struct represented a price for an asset posted by a specific oracle +type PostedPrice struct { + AssetCode string `json:"asset_code" yaml:"asset_code"` + 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(`AssetCode: %s +Price: %s`, cp.AssetCode, cp.Price)) +} + +// 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]) } 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..c6be85a7 100644 --- a/x/pricefeed/types/params.go +++ b/x/pricefeed/types/params.go @@ -8,74 +8,48 @@ import ( params "github.com/cosmos/cosmos-sdk/x/params/subspace" ) -// Parameter store key var ( - // ParamStoreKeyOracles Param store key for oracles - ParamStoreKeyOracles = []byte("oracles") - // ParamStoreKeyAssets Param store key for assets - ParamStoreKeyAssets = []byte("assets") + // KeyAssets store key for assets + KeyAssets = []byte("assets") ) // 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 { + Assets []Asset `json:"assets" yaml:"assets"` // Array containing the assets supported by the pricefeed } -// NewAssetParams creates a new AssetParams object -func NewAssetParams(assets []Asset) AssetParams { - return AssetParams{ +// 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: KeyAssets, Value: &p.Assets}, + } +} + +// NewParams creates a new AssetParams object +func NewParams(assets []Asset) Params { + return Params{ Assets: assets, } } -// DefaultAssetParams default params for assets -func DefaultAssetParams() AssetParams { - return NewAssetParams([]Asset{}) +// DefaultParams default params for pricefeed +func DefaultParams() Params { + return NewParams(Assets{}) } -// implements fmt.stringer -func (ap AssetParams) String() string { - var assetListString []string - for _, asset := range ap.Assets { - assetListString = append(assetListString, asset.String()) +// String implements fmt.stringer +func (p Params) String() string { + out := "Params:\n" + for _, a := range p.Assets { + out += a.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, - } -} - -// DefaultOracleParams default params for assets -func DefaultOracleParams() OracleParams { - return NewOracleParams([]Oracle{}) -} - -// implements fmt.stringer -func (op OracleParams) String() string { - var oracleListString []string - for _, oracle := range op.Oracles { - oracleListString = append(oracleListString, oracle.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.Assets { + if asset.AssetCode == "" { + return fmt.Errorf("invalid asset: %s. missing asset code", 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]) }