feat: draft params for pricefeed

This commit is contained in:
Kevin Davis 2019-11-27 09:45:59 -05:00
parent 57a1532db4
commit d5da161dd8
21 changed files with 620 additions and 631 deletions

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 h1:yb+E8HGzFnO0YwLS6OCBIAVWtN8KfCYoKeO9mgAmQN0=
github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1/go.mod h1:IGBhkbOK1ebLqMWjtgo99zUxWHsA5IOb6N9CI8nHs0Y= 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.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 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI=
github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= 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= github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J96OTjeFFc=

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

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

View File

@ -2,9 +2,11 @@
// autogenerated code using github.com/rigelrozanski/multitool // autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories: // aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/ // ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/
// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/keeper/
package pricefeed package pricefeed
import ( import (
"github.com/kava-labs/kava/x/pricefeed/keeper"
"github.com/kava-labs/kava/x/pricefeed/types" "github.com/kava-labs/kava/x/pricefeed/types"
) )
@ -18,6 +20,12 @@ const (
ModuleName = types.ModuleName ModuleName = types.ModuleName
StoreKey = types.StoreKey StoreKey = types.StoreKey
RouterKey = types.RouterKey RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
RawPriceFeedPrefix = types.RawPriceFeedPrefix
CurrentPricePrefix = types.CurrentPricePrefix
AssetPrefix = types.AssetPrefix
OraclePrefix = types.OraclePrefix
TypeMsgPostPrice = types.TypeMsgPostPrice TypeMsgPostPrice = types.TypeMsgPostPrice
QueryCurrentPrice = types.QueryCurrentPrice QueryCurrentPrice = types.QueryCurrentPrice
QueryRawPrices = types.QueryRawPrices QueryRawPrices = types.QueryRawPrices
@ -37,28 +45,29 @@ var (
ValidateGenesis = types.ValidateGenesis ValidateGenesis = types.ValidateGenesis
NewMsgPostPrice = types.NewMsgPostPrice NewMsgPostPrice = types.NewMsgPostPrice
ParamKeyTable = types.ParamKeyTable ParamKeyTable = types.ParamKeyTable
NewAssetParams = types.NewAssetParams NewParams = types.NewParams
DefaultAssetParams = types.DefaultAssetParams DefaultParams = types.DefaultParams
NewOracleParams = types.NewOracleParams NewKeeper = keeper.NewKeeper
DefaultOracleParams = types.DefaultOracleParams NewQuerier = keeper.NewQuerier
// variable aliases // variable aliases
ModuleCdc = types.ModuleCdc ModuleCdc = types.ModuleCdc
ParamStoreKeyOracles = types.ParamStoreKeyOracles KeyAssets = types.KeyAssets
ParamStoreKeyAssets = types.ParamStoreKeyAssets
) )
type ( type (
GenesisState = types.GenesisState GenesisState = types.GenesisState
MsgPostPrice = types.MsgPostPrice MsgPostPrice = types.MsgPostPrice
AssetParams = types.AssetParams Params = types.Params
OracleParams = types.OracleParams
ParamSubspace = types.ParamSubspace ParamSubspace = types.ParamSubspace
QueryRawPricesResp = types.QueryRawPricesResp QueryRawPricesResp = types.QueryRawPricesResp
QueryAssetsResp = types.QueryAssetsResp QueryAssetsResp = types.QueryAssetsResp
Asset = types.Asset Asset = types.Asset
Assets = types.Assets
Oracle = types.Oracle Oracle = types.Oracle
Oracles = types.Oracles
CurrentPrice = types.CurrentPrice CurrentPrice = types.CurrentPrice
PostedPrice = types.PostedPrice PostedPrice = types.PostedPrice
SortDecs = types.SortDecs SortDecs = types.SortDecs
Keeper = keeper.Keeper
) )

View File

@ -2,6 +2,7 @@ package cli
import ( import (
"fmt" "fmt"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -12,6 +13,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/pricefeed/types" "github.com/kava-labs/kava/x/pricefeed/types"
tmtime "github.com/tendermint/tendermint/types/time"
) )
// GetTxCmd returns the transaction commands for this module // GetTxCmd returns the transaction commands for this module
@ -47,11 +49,13 @@ func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
expiry, ok := sdk.NewIntFromString(args[2]) expiryInt, ok := sdk.NewIntFromString(args[2])
if !ok { if !ok {
fmt.Printf("invalid expiry - %s \n", args[2]) fmt.Printf("invalid expiry - %s \n", args[2])
return nil return nil
} }
expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0))
msg := types.NewMsgPostPrice(cliCtx.GetFromAddress(), args[0], price, expiry) msg := types.NewMsgPostPrice(cliCtx.GetFromAddress(), args[0], price, expiry)
err = msg.ValidateBasic() err = msg.ValidateBasic()
if err != nil { if err != nil {

View File

@ -3,6 +3,7 @@ package rest
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
@ -10,6 +11,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/kava-labs/kava/x/pricefeed/types" "github.com/kava-labs/kava/x/pricefeed/types"
tmtime "github.com/tendermint/tendermint/types/time"
) )
const ( const (
@ -57,11 +59,12 @@ func postPriceHandler(cliCtx context.CLIContext) http.HandlerFunc {
return return
} }
expiry, ok := sdk.NewIntFromString(req.Expiry) expiryInt, ok := sdk.NewIntFromString(req.Expiry)
if !ok { if !ok {
rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry") rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry")
return return
} }
expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0))
// create the message // create the message
msg := types.NewMsgPostPrice(addr, req.AssetCode, price, expiry) msg := types.NewMsgPostPrice(addr, req.AssetCode, price, expiry)

View File

@ -4,29 +4,25 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
// InitGenesis sets distribution information for genesis. // InitGenesis sets distribution information for genesis.
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
// Set the assets and oracles from params // Set the assets and oracles from params
keeper.SetAssetParams(ctx, data.AssetParams) keeper.SetParams(ctx, data.Params)
keeper.SetOracleParams(ctx ,data.OracleParams)
// Iterate through the posted prices and set them in the store // Iterate through the posted prices and set them in the store
for _, pp := range data.PostedPrices { for _, pp := range data.PostedPrices {
addr, err := sdk.AccAddressFromBech32(pp.OracleAddress) _, err := keeper.SetPrice(ctx, pp.OracleAddress, pp.AssetCode, pp.Price, pp.Expiry)
if err != nil {
panic(err)
}
_, err = keeper.SetPrice(ctx, addr, pp.AssetCode, pp.Price, pp.Expiry)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
// Set the current price (if any) based on what's now in the store // Set the current price (if any) based on what's now in the store
if err := keeper.SetCurrentPrices(ctx); err != nil { for _, a := range data.Params.Assets {
panic(err) 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 { func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
// Get the params for assets and oracles // Get the params for assets and oracles
assetParams := keeper.GetAssetParams(ctx) params := keeper.GetParams(ctx)
oracleParams := keeper.GetOracleParams(ctx)
var postedPrices []PostedPrice var postedPrices []PostedPrice
for _, asset := range keeper.GetAssets(ctx) { for _, asset := range keeper.GetAssetParams(ctx) {
pp := keeper.GetRawPrices(ctx, asset.AssetCode) pp := keeper.GetRawPrices(ctx, asset.AssetCode)
postedPrices = append(postedPrices, pp...) postedPrices = append(postedPrices, pp...)
} }
return GenesisState{ return GenesisState{
AssetParams: assetParams, Params: params,
OracleParams: oracleParams,
PostedPrices: postedPrices, PostedPrices: postedPrices,
} }
} }

View File

@ -29,22 +29,10 @@ func HandleMsgPostPrice(
msg MsgPostPrice) sdk.Result { msg MsgPostPrice) sdk.Result {
// TODO cleanup message validation and errors // TODO cleanup message validation and errors
err := k.ValidatePostPrice(ctx, msg) _, err := k.GetOracle(ctx, msg.AssetCode, msg.From)
if err != nil { if err != nil {
return err.Result() return ErrInvalidOracle(k.Codespace()).Result()
} }
k.SetPrice(ctx, msg.From, msg.AssetCode, msg.Price, msg.Expiry) k.SetPrice(ctx, msg.From, msg.AssetCode, msg.Price, msg.Expiry)
return sdk.Result{} 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(
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
}

View File

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

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

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.GetAssetParams(ctx))
}
// SetParams updates params in the store
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramstore.SetParamSet(ctx, &params)
}
// 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
}

View File

@ -1,21 +1,23 @@
package pricefeed package keeper
import ( import (
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
abci "github.com/tendermint/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/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 // NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier { func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] { switch path[0] {
case QueryCurrentPrice: case types.QueryCurrentPrice:
return queryCurrentPrice(ctx, path[1:], req, keeper) return queryCurrentPrice(ctx, path[1:], req, keeper)
case QueryRawPrices: case types.QueryRawPrices:
return queryRawPrices(ctx, path[1:], req, keeper) return queryRawPrices(ctx, path[1:], req, keeper)
case QueryAssets: case types.QueryAssets:
return queryAssets(ctx, req, keeper) return queryAssets(ctx, req, keeper)
default: default:
return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint") 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) { 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] assetCode := path[0]
_, found := keeper.GetAsset(ctx, assetCode) _, found := keeper.GetAsset(ctx, assetCode)
if !found { 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) { func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var assetList QueryAssetsResp var assetList types.QueryAssetsResp
assets := keeper.GetAssets(ctx) assets := keeper.GetAssetParams(ctx)
for _, asset := range assets { for _, asset := range assets {
assetList = append(assetList, asset.String()) assetList = append(assetList, asset.String())
} }

View File

@ -1,4 +1,4 @@
package pricefeed package keeper
import ( import (
"testing" "testing"
@ -7,8 +7,9 @@ import (
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/mock" "github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto"
"github.com/kava-labs/kava/x/pricefeed/types"
) )
type testHelper struct { type testHelper struct {
@ -19,16 +20,12 @@ type testHelper struct {
privKeys []crypto.PrivKey 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() mApp := mock.NewApp()
RegisterCodec(mApp.Cdc) types.RegisterCodec(mApp.Cdc)
keyPricefeed := sdk.NewKVStoreKey(StoreKey) keyPricefeed := sdk.NewKVStoreKey(types.StoreKey)
pk := mApp.ParamsKeeper pk := mApp.ParamsKeeper
keeper := NewKeeper(keyPricefeed, mApp.Cdc, pk.Subspace(DefaultParamspace).WithKeyTable(ParamKeyTable()), DefaultCodespace) keeper := NewKeeper(keyPricefeed, mApp.Cdc, pk.Subspace(types.DefaultParamspace), types.DefaultCodespace)
// Register routes
mApp.Router().AddRoute(RouterKey, NewHandler(keeper))
mApp.SetEndBlocker(getEndBlocker(keeper))
require.NoError(t, mApp.CompleteSetup(keyPricefeed)) 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) mock.SetGenesis(mApp, genAccs)
return testHelper{mApp, keeper, addrs, pubKeys, privKeys} 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"
)
// 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]) }

View File

@ -2,21 +2,18 @@ package types
import ( import (
"bytes" "bytes"
"fmt"
) )
// GenesisState - pricefeed state that must be provided at genesis // GenesisState - pricefeed state that must be provided at genesis
type GenesisState struct { type GenesisState struct {
AssetParams AssetParams `json:"asset_params" yaml:"asset_params"` Params Params `json:"asset_params" yaml:"asset_params"`
OracleParams OracleParams `json:"oracle_params" yaml:"oracle_params"`
PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"` PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"`
} }
// NewGenesisState creates a new genesis state for the pricefeed module // 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{ return GenesisState{
AssetParams: ap, Params: p,
OracleParams: op,
PostedPrices: pp, PostedPrices: pp,
} }
} }
@ -24,8 +21,7 @@ func NewGenesisState(ap AssetParams, op OracleParams, pp []PostedPrice) GenesisS
// DefaultGenesisState defines default GenesisState for pricefeed // DefaultGenesisState defines default GenesisState for pricefeed
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return NewGenesisState( return NewGenesisState(
DefaultAssetParams(), DefaultParams(),
DefaultOracleParams(),
[]PostedPrice{}, []PostedPrice{},
) )
} }
@ -45,19 +41,9 @@ func (data GenesisState) IsEmpty() bool {
// ValidateGenesis performs basic validation of genesis data returning an // ValidateGenesis performs basic validation of genesis data returning an
// error for any failed validation criteria. // error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error { 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 if err := data.Params.Validate(); err != nil {
for _, oracle := range data.OracleParams.Oracles { return err
if oracle.OracleAddress == "" {
return fmt.Errorf("invalid oracle: %s. missing oracle address", oracle.String())
} }
}
return nil return nil
} }

View File

@ -9,4 +9,22 @@ const (
// RouterKey Top level router key // RouterKey Top level router key
RouterKey = ModuleName 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 package types
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
@ -15,7 +17,7 @@ type MsgPostPrice struct {
From sdk.AccAddress // client that sent in this address From sdk.AccAddress // client that sent in this address
AssetCode string // asset code used by exchanges/api AssetCode string // asset code used by exchanges/api
Price sdk.Dec // price in decimal (max precision 18) 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 // NewMsgPostPrice creates a new post price msg
@ -23,7 +25,7 @@ func NewMsgPostPrice(
from sdk.AccAddress, from sdk.AccAddress,
assetCode string, assetCode string,
price sdk.Dec, price sdk.Dec,
expiry sdk.Int) MsgPostPrice { expiry time.Time) MsgPostPrice {
return MsgPostPrice{ return MsgPostPrice{
From: from, From: from,
AssetCode: assetCode, AssetCode: assetCode,
@ -60,9 +62,6 @@ func (msg MsgPostPrice) ValidateBasic() sdk.Error {
if msg.Price.LT(sdk.ZeroDec()) { if msg.Price.LT(sdk.ZeroDec()) {
return sdk.ErrInternal("invalid (negative) price") return sdk.ErrInternal("invalid (negative) price")
} }
if msg.Expiry.LT(sdk.ZeroInt()) {
return sdk.ErrInternal("invalid (negative) expiry")
}
// TODO check coin denoms // TODO check coin denoms
return nil return nil
} }

View File

@ -5,6 +5,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
tmtime "github.com/tendermint/tendermint/types/time"
) )
func TestMsgPlaceBid_ValidateBasic(t *testing.T) { func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
@ -13,8 +14,7 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
// OracleAddress: addr.String(), // OracleAddress: addr.String(),
// }} // }}
price, _ := sdk.NewDecFromStr("0.3005") price, _ := sdk.NewDecFromStr("0.3005")
expiry, _ := sdk.NewIntFromString("10") expiry := tmtime.Now()
negativeExpiry, _ := sdk.NewIntFromString("-3")
negativePrice, _ := sdk.NewDecFromStr("-3.05") negativePrice, _ := sdk.NewDecFromStr("-3.05")
tests := []struct { tests := []struct {
@ -26,7 +26,6 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
{"emptyAddr", MsgPostPrice{sdk.AccAddress{}, "xrp", price, expiry}, false}, {"emptyAddr", MsgPostPrice{sdk.AccAddress{}, "xrp", price, expiry}, false},
{"emptyAsset", MsgPostPrice{addr, "", price, expiry}, false}, {"emptyAsset", MsgPostPrice{addr, "", price, expiry}, false},
{"negativePrice", MsgPostPrice{addr, "xrp", negativePrice, expiry}, false}, {"negativePrice", MsgPostPrice{addr, "xrp", negativePrice, expiry}, false},
{"negativeExpiry", MsgPostPrice{addr, "xrp", price, negativeExpiry}, false},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {

View File

@ -8,74 +8,48 @@ import (
params "github.com/cosmos/cosmos-sdk/x/params/subspace" params "github.com/cosmos/cosmos-sdk/x/params/subspace"
) )
// Parameter store key
var ( var (
// ParamStoreKeyOracles Param store key for oracles // KeyAssets store key for assets
ParamStoreKeyOracles = []byte("oracles") KeyAssets = []byte("assets")
// ParamStoreKeyAssets Param store key for assets
ParamStoreKeyAssets = []byte("assets")
) )
// ParamKeyTable Key declaration for parameters // ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable { func ParamKeyTable() params.KeyTable {
return params.NewKeyTable( return params.NewKeyTable().RegisterParamSet(&Params{})
ParamStoreKeyOracles, OracleParams{},
ParamStoreKeyAssets, AssetParams{},
)
} }
// AssetParams params for assets. Can be altered via governance // Params params for pricefeed. Can be altered via governance
type AssetParams struct { type Params struct {
Assets []Asset `json:"assets,omitempty" yaml:"assets,omitempty"` // Array containing the assets supported by the pricefeed Assets []Asset `json:"assets" yaml:"assets"` // Array containing the assets supported by the pricefeed
} }
// NewAssetParams creates a new AssetParams object // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func NewAssetParams(assets []Asset) AssetParams { // pairs of pricefeed module's parameters.
return AssetParams{ 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, Assets: assets,
} }
} }
// DefaultAssetParams default params for assets // DefaultParams default params for pricefeed
func DefaultAssetParams() AssetParams { func DefaultParams() Params {
return NewAssetParams([]Asset{}) return NewParams(Assets{})
} }
// implements fmt.stringer // String implements fmt.stringer
func (ap AssetParams) String() string { func (p Params) String() string {
var assetListString []string out := "Params:\n"
for _, asset := range ap.Assets { for _, a := range p.Assets {
assetListString = append(assetListString, asset.String()) out += a.String()
} }
return strings.TrimSpace(fmt.Sprintf(`Asset Params: return strings.TrimSpace(out)
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, ", ")))
} }
// ParamSubspace defines the expected Subspace interface for parameters // ParamSubspace defines the expected Subspace interface for parameters
@ -83,3 +57,14 @@ type ParamSubspace interface {
Get(ctx sdk.Context, key []byte, ptr interface{}) Get(ctx sdk.Context, key []byte, ptr interface{})
Set(ctx sdk.Context, key []byte, param 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
}

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