Merge branch 'kd-propose-params' into ro-remove-mock-from-tests

This commit is contained in:
rhuairahrighairigh 2019-12-05 15:56:24 +00:00
commit 688b7830c0
30 changed files with 724 additions and 700 deletions

View File

@ -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(

1
go.sum
View File

@ -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=

View File

@ -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()

View File

@ -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 {

View File

@ -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))

View File

@ -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
}

View File

@ -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{},
}
}

View File

@ -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))

View File

@ -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),
},
},
}

28
x/pricefeed/abci.go Normal file
View File

@ -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
}

View File

@ -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
)

View File

@ -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 {

View File

@ -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)

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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, &params)
}
// 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
}

View File

@ -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())
}

View File

@ -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{}
}
}

View File

@ -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)
}

View File

@ -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]) }

View File

@ -0,0 +1,8 @@
package types
// Pricefeed module event types
const (
EventTypeNoValidPrices = "no_valid_prices"
AttributeKeyPriceUpdateFailed = "price_update_failed"
)

View File

@ -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
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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]) }