Merge pull request #272 from Kava-Labs/kd-propose-params

Params and types for pricefeed
This commit is contained in:
Kevin Davis 2019-12-05 11:03:41 -05:00 committed by GitHub
commit 6c42e78e25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 727 additions and 701 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=

View File

@ -2,10 +2,12 @@ package cdp
import ( import (
"testing" "testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock" "github.com/cosmos/cosmos-sdk/x/mock"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
) )
@ -19,7 +21,7 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) {
mock.SetGenesis(mapp, genAccs) mock.SetGenesis(mapp, genAccs)
mock.CheckBalance(t, mapp, testAddr, cs(c("xrp", 100))) mock.CheckBalance(t, mapp, testAddr, cs(c("xrp", 100)))
// setup pricefeed, TODO can this be shortened a bit? // setup pricefeed, TODO can this be shortened a bit?
header := abci.Header{Height: mapp.LastBlockHeight() + 1} header := abci.Header{Height: mapp.LastBlockHeight() + 1, Time: tmtime.Now()}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header) ctx := mapp.BaseApp.NewContext(false, header)
params := CdpParams{ params := CdpParams{
@ -35,15 +37,19 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) {
} }
keeper.SetParams(ctx, params) keeper.SetParams(ctx, params)
keeper.SetGlobalDebt(ctx, sdk.NewInt(0)) keeper.SetGlobalDebt(ctx, sdk.NewInt(0))
ap := pricefeed.AssetParams{ ap := pricefeed.Params{
Assets: []pricefeed.Asset{pricefeed.Asset{AssetCode: "xrp", Description: ""}}, Markets: []pricefeed.Market{
pricefeed.Market{
MarketID: "xrp", BaseAsset: "xrp",
QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
},
} }
pfKeeper.SetAssetParams(ctx, ap) pfKeeper.SetParams(ctx, ap)
pfKeeper.SetPrice( pfKeeper.SetPrice(
ctx, sdk.AccAddress{}, "xrp", ctx, sdk.AccAddress{}, "xrp",
sdk.MustNewDecFromStr("1.00"), sdk.MustNewDecFromStr("1.00"),
sdk.NewInt(10)) header.Time.Add(time.Hour*1))
pfKeeper.SetCurrentPrices(ctx) pfKeeper.SetCurrentPrices(ctx, "xrp")
mapp.EndBlock(abci.RequestEndBlock{}) mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit() mapp.Commit()

View File

@ -9,9 +9,9 @@ import (
func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, data GenesisState) { 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 // validate denoms - check that any collaterals in the CdpParams are in the pricefeed, pricefeed needs to initgenesis before cdp
collateralMap := make(map[string]int) collateralMap := make(map[string]int)
ap := pk.GetAssetParams(ctx) ap := pk.GetParams(ctx)
for _, a := range ap.Assets { for _, m := range ap.Markets {
collateralMap[a.AssetCode] = 1 collateralMap[m.MarketID] = 1
} }
for _, col := range data.Params.CollateralParams { for _, col := range data.Params.CollateralParams {

View File

@ -3,6 +3,7 @@ package keeper
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
@ -12,6 +13,7 @@ import (
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
) )
// How could one reduce the number of params in the test cases. Create a table driven test for each of the 4 add/withdraw collateral/debt? // How could one reduce the number of params in the test cases. Create a table driven test for each of the 4 add/withdraw collateral/debt?
@ -103,27 +105,29 @@ func TestKeeper_ModifyCDP(t *testing.T) {
} }
mock.SetGenesis(mapp, []authexported.Account{&genAcc}) mock.SetGenesis(mapp, []authexported.Account{&genAcc})
// create a new context // create a new context
header := abci.Header{Height: mapp.LastBlockHeight() + 1} header := abci.Header{Height: mapp.LastBlockHeight() + 1, Time: tmtime.Now()}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header) ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle()) keeper.SetParams(ctx, defaultParamsSingle())
// setup store state // setup store state
ap := pricefeed.AssetParams{ ap := pricefeed.Params{
Assets: []pricefeed.Asset{ Markets: []pricefeed.Market{
pricefeed.Asset{AssetCode: "xrp", Description: ""}, pricefeed.Market{
MarketID: "xrp", BaseAsset: "xrp",
QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
}, },
} }
keeper.pricefeedKeeper.SetAssetParams(ctx, ap) keeper.pricefeedKeeper.SetParams(ctx, ap)
_, err := keeper.pricefeedKeeper.SetPrice( _, err := keeper.pricefeedKeeper.SetPrice(
ctx, ownerAddr, "xrp", ctx, ownerAddr, "xrp",
sdk.MustNewDecFromStr(tc.price), sdk.MustNewDecFromStr(tc.price),
sdk.NewInt(ctx.BlockHeight()+10)) header.Time.Add(time.Hour*1))
if err != nil { if err != nil {
t.Log("test context height", ctx.BlockHeight()) t.Log("test context height", ctx.BlockHeight())
t.Log(err) t.Log(err)
t.Log(tc.name) t.Log(tc.name)
} }
err = keeper.pricefeedKeeper.SetCurrentPrices(ctx) err = keeper.pricefeedKeeper.SetCurrentPrices(ctx, "xrp")
if err != nil { if err != nil {
t.Log("test context height", ctx.BlockHeight()) t.Log("test context height", ctx.BlockHeight())
t.Log(err) t.Log(err)
@ -144,9 +148,6 @@ func TestKeeper_ModifyCDP(t *testing.T) {
// get new state for verification // get new state for verification
actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom) actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom)
if tc.name == "removeTooMuchCollateral" {
t.Log(actualCDP.String())
}
// check for err // check for err
if tc.expectPass { if tc.expectPass {
@ -180,21 +181,23 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) {
testAddr := addrs[0] testAddr := addrs[0]
mock.SetGenesis(mapp, genAccs) mock.SetGenesis(mapp, genAccs)
// setup pricefeed // setup pricefeed
header := abci.Header{Height: mapp.LastBlockHeight() + 1} header := abci.Header{Height: mapp.LastBlockHeight() + 1, Time: tmtime.Now()}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header) ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle()) keeper.SetParams(ctx, defaultParamsSingle())
ap := pricefeed.AssetParams{ ap := pricefeed.Params{
Assets: []pricefeed.Asset{ Markets: []pricefeed.Market{
pricefeed.Asset{AssetCode: "xrp", Description: ""}, pricefeed.Market{
MarketID: "xrp", BaseAsset: "xrp",
QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
}, },
} }
keeper.pricefeedKeeper.SetAssetParams(ctx, ap) keeper.pricefeedKeeper.SetParams(ctx, ap)
keeper.pricefeedKeeper.SetPrice( keeper.pricefeedKeeper.SetPrice(
ctx, sdk.AccAddress{}, collateral, ctx, sdk.AccAddress{}, collateral,
sdk.MustNewDecFromStr("1.00"), sdk.MustNewDecFromStr("1.00"),
i(10)) header.Time.Add(time.Hour*1))
keeper.pricefeedKeeper.SetCurrentPrices(ctx) keeper.pricefeedKeeper.SetCurrentPrices(ctx, collateral)
// Create CDP // Create CDP
keeper.SetGlobalDebt(ctx, i(0)) keeper.SetGlobalDebt(ctx, i(0))
err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5)) err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5))
@ -203,8 +206,8 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) {
keeper.pricefeedKeeper.SetPrice( keeper.pricefeedKeeper.SetPrice(
ctx, sdk.AccAddress{}, collateral, ctx, sdk.AccAddress{}, collateral,
sdk.MustNewDecFromStr("0.90"), sdk.MustNewDecFromStr("0.90"),
i(10)) header.Time.Add(time.Hour*1))
keeper.pricefeedKeeper.SetCurrentPrices(ctx) keeper.pricefeedKeeper.SetCurrentPrices(ctx, collateral)
// Seize entire CDP // Seize entire CDP
err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5)) err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5))

View File

@ -30,7 +30,7 @@ func setUpMockAppWithoutGenesis() (*mock.App, Keeper, []sdk.AccAddress, []crypto
keyCDP := sdk.NewKVStoreKey("cdp") keyCDP := sdk.NewKVStoreKey("cdp")
keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey) keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey)
pk := mapp.ParamsKeeper pk := mapp.ParamsKeeper
priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace), pricefeed.DefaultCodespace)
blacklistedAddrs := make(map[string]bool) blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs) bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
cdpKeeper := NewKeeper(mapp.Cdc, keyCDP, pk.Subspace(types.DefaultParamspace), priceFeedKeeper, bankKeeper) cdpKeeper := NewKeeper(mapp.Cdc, keyCDP, pk.Subspace(types.DefaultParamspace), priceFeedKeeper, bankKeeper)

View File

@ -28,7 +28,7 @@ func setUpMockAppWithoutGenesis() (*mock.App, Keeper, PricefeedKeeper) {
keyCDP := sdk.NewKVStoreKey("cdp") keyCDP := sdk.NewKVStoreKey("cdp")
keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey) keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey)
pk := mapp.ParamsKeeper pk := mapp.ParamsKeeper
priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace), pricefeed.DefaultCodespace)
blacklistedAddrs := make(map[string]bool) blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs) bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
cdpKeeper := NewKeeper(mapp.Cdc, keyCDP, pk.Subspace(DefaultParamspace), priceFeedKeeper, bankKeeper) cdpKeeper := NewKeeper(mapp.Cdc, keyCDP, pk.Subspace(DefaultParamspace), priceFeedKeeper, bankKeeper)

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"
pftypes "github.com/kava-labs/kava/x/pricefeed/types" pftypes "github.com/kava-labs/kava/x/pricefeed/types"
) )
@ -14,9 +16,9 @@ type BankKeeper interface {
type PricefeedKeeper interface { type PricefeedKeeper interface {
GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice 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 // These are used for testing TODO replace mockApp with keeper in tests to remove these
SetAssetParams(sdk.Context, pftypes.AssetParams) SetParams(sdk.Context, pftypes.Params)
SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, sdk.Int) (pftypes.PostedPrice, sdk.Error) SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, sdk.Error)
SetCurrentPrices(sdk.Context) sdk.Error SetCurrentPrices(sdk.Context, string) sdk.Error
} }

View File

@ -1,8 +1,6 @@
package types package types
import ( import sdk "github.com/cosmos/cosmos-sdk/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GenesisState is the state that must be provided at genesis. // 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 // TODO What is globaldebt and is is separate from the global debt limit in CdpParams
@ -19,7 +17,6 @@ type GenesisState struct {
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return GenesisState{ return GenesisState{
Params: DefaultParams(), Params: DefaultParams(),
GlobalDebt: sdk.ZeroInt(),
CDPs: CDPs{}, CDPs: CDPs{},
} }
} }

View File

@ -2,10 +2,12 @@ package keeper
import ( import (
"testing" "testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock" "github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require" "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/cdp"
"github.com/kava-labs/kava/x/liquidator/types" "github.com/kava-labs/kava/x/liquidator/types"
@ -19,8 +21,8 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) {
_, addrs := mock.GeneratePrivKeyAddressPairs(1) _, addrs := mock.GeneratePrivKeyAddressPairs(1)
pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis()) pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis())
k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999)) k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1))
k.pricefeedKeeper.SetCurrentPrices(ctx) k.pricefeedKeeper.SetCurrentPrices(ctx, "btc")
cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis()) cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis())
dp := defaultParams() dp := defaultParams()
k.liquidatorKeeper.SetParams(ctx, dp) 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.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.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1))
k.pricefeedKeeper.SetCurrentPrices(ctx) k.pricefeedKeeper.SetCurrentPrices(ctx, "btc")
// Run test function // Run test function
auctionID, err := k.liquidatorKeeper.SeizeAndStartCollateralAuction(ctx, addrs[0], "btc") 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()) pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis())
k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999)) k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1))
k.pricefeedKeeper.SetCurrentPrices(ctx) k.pricefeedKeeper.SetCurrentPrices(ctx, "btc")
k.bankKeeper.AddCoins(ctx, addrs[0], cs(c("btc", 100))) k.bankKeeper.AddCoins(ctx, addrs[0], cs(c("btc", 100)))
cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis()) cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis())
k.liquidatorKeeper.SetParams(ctx, defaultParams()) k.liquidatorKeeper.SetParams(ctx, defaultParams())
k.cdpKeeper.ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) 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.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1))
k.pricefeedKeeper.SetCurrentPrices(ctx) k.pricefeedKeeper.SetCurrentPrices(ctx, "btc")
// Run test function // Run test function
err := k.liquidatorKeeper.partialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000)) err := k.liquidatorKeeper.partialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000))

View File

@ -1,6 +1,8 @@
package keeper package keeper
import ( import (
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store" "github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
@ -9,6 +11,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/params"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/libs/log"
tmtime "github.com/tendermint/tendermint/types/time"
dbm "github.com/tendermint/tm-db" dbm "github.com/tendermint/tm-db"
"github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/auction"
@ -75,7 +78,7 @@ func setupTestKeepers() (sdk.Context, keepers) {
bank.DefaultCodespace, bank.DefaultCodespace,
blacklistedAddrs, blacklistedAddrs,
) )
pricefeedKeeper := pricefeed.NewKeeper(keyPriceFeed, cdc, paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) pricefeedKeeper := pricefeed.NewKeeper(keyPriceFeed, cdc, paramsKeeper.Subspace(pricefeed.DefaultParamspace), pricefeed.DefaultCodespace)
cdpKeeper := cdp.NewKeeper( cdpKeeper := cdp.NewKeeper(
cdc, cdc,
keyCDP, keyCDP,
@ -150,20 +153,18 @@ func cdpDefaultGenesis() cdp.GenesisState {
} }
func pricefeedGenesis() pricefeed.GenesisState { func pricefeedGenesis() pricefeed.GenesisState {
ap := pricefeed.AssetParams{ ap := pricefeed.Params{
Assets: []pricefeed.Asset{ Markets: []pricefeed.Market{
pricefeed.Asset{AssetCode: "btc", Description: "a description"}, pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}},
},
} }
return pricefeed.GenesisState{ return pricefeed.GenesisState{
AssetParams: ap, Params: ap,
OracleParams: pricefeed.DefaultOracleParams(),
PostedPrices: []pricefeed.PostedPrice{ PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{ pricefeed.PostedPrice{
AssetCode: "btc", MarketID: "btc",
OracleAddress: "", OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("8000.00"), 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,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"
) )
@ -15,9 +17,17 @@ const (
CodeInvalidPrice = types.CodeInvalidPrice CodeInvalidPrice = types.CodeInvalidPrice
CodeInvalidAsset = types.CodeInvalidAsset CodeInvalidAsset = types.CodeInvalidAsset
CodeInvalidOracle = types.CodeInvalidOracle CodeInvalidOracle = types.CodeInvalidOracle
EventTypeNoValidPrices = types.EventTypeNoValidPrices
AttributeKeyPriceUpdateFailed = types.AttributeKeyPriceUpdateFailed
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 +47,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 KeyMarkets = types.KeyMarkets
ParamStoreKeyAssets = types.ParamStoreKeyAssets
) )
type ( type (
GenesisState = types.GenesisState Market = types.Market
MsgPostPrice = types.MsgPostPrice Markets = types.Markets
AssetParams = types.AssetParams
OracleParams = types.OracleParams
ParamSubspace = types.ParamSubspace
QueryRawPricesResp = types.QueryRawPricesResp
QueryAssetsResp = types.QueryAssetsResp
Asset = types.Asset
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
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 ( 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.MarketID, 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.Markets {
panic(err) if a.Active {
_ = keeper.SetCurrentPrices(ctx, a.MarketID)
}
} }
} }
@ -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.GetMarketParams(ctx) {
pp := keeper.GetRawPrices(ctx, asset.AssetCode) pp := keeper.GetRawPrices(ctx, asset.MarketID)
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,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(
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,
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 ( 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")
@ -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) { func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
assetCode := path[0] marketID := path[0]
_, found := keeper.GetAsset(ctx, assetCode) _, found := keeper.GetMarket(ctx, marketID)
if !found { if !found {
return []byte{}, sdk.ErrUnknownRequest("asset not 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) bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice)
if err2 != nil { 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) { 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] marketID := path[0]
_, found := keeper.GetAsset(ctx, assetCode) _, found := keeper.GetMarket(ctx, marketID)
if !found { if !found {
return []byte{}, sdk.ErrUnknownRequest("asset not found") return []byte{}, sdk.ErrUnknownRequest("asset not found")
} }
rawPrices := keeper.GetRawPrices(ctx, assetCode) rawPrices := keeper.GetRawPrices(ctx, marketID)
for _, price := range rawPrices { for _, price := range rawPrices {
priceList = append(priceList, price.String()) 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) { 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.GetMarketParams(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"
)
// 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 ( 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

@ -5,77 +5,51 @@ import (
"strings" "strings"
sdk "github.com/cosmos/cosmos-sdk/types" 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 ( var (
// ParamStoreKeyOracles Param store key for oracles // KeyMarkets store key for markets
ParamStoreKeyOracles = []byte("oracles") KeyMarkets = []byte("Markets")
// 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 Markets Markets `json:"markets" yaml:"markets"` // Array containing the markets 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 {
Assets: assets, return params.ParamSetPairs{
{Key: KeyMarkets, Value: &p.Markets},
} }
} }
// DefaultAssetParams default params for assets // NewParams creates a new AssetParams object
func DefaultAssetParams() AssetParams { func NewParams(markets Markets) Params {
return NewAssetParams([]Asset{}) return Params{
} Markets: markets,
// 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,
} }
} }
// DefaultOracleParams default params for assets // DefaultParams default params for pricefeed
func DefaultOracleParams() OracleParams { func DefaultParams() Params {
return NewOracleParams([]Oracle{}) return NewParams(Markets{})
} }
// implements fmt.stringer // String implements fmt.stringer
func (op OracleParams) String() string { func (p Params) String() string {
var oracleListString []string out := "Params:\n"
for _, oracle := range op.Oracles { for _, a := range p.Markets {
oracleListString = append(oracleListString, oracle.String()) out += a.String()
} }
return strings.TrimSpace(fmt.Sprintf(`Oracle Params: return strings.TrimSpace(out)
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.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]) }