mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
x/committee: committee gov module
Committee Gov Module
This commit is contained in:
commit
e9c16fa752
49
app/app.go
49
app/app.go
@ -4,14 +4,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/kava-labs/kava/x/auction"
|
|
||||||
"github.com/kava-labs/kava/x/bep3"
|
|
||||||
"github.com/kava-labs/kava/x/cdp"
|
|
||||||
"github.com/kava-labs/kava/x/incentive"
|
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
|
||||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
|
||||||
|
|
||||||
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"
|
||||||
tmos "github.com/tendermint/tendermint/libs/os"
|
tmos "github.com/tendermint/tendermint/libs/os"
|
||||||
@ -37,6 +29,15 @@ import (
|
|||||||
"github.com/cosmos/cosmos-sdk/x/slashing"
|
"github.com/cosmos/cosmos-sdk/x/slashing"
|
||||||
"github.com/cosmos/cosmos-sdk/x/staking"
|
"github.com/cosmos/cosmos-sdk/x/staking"
|
||||||
"github.com/cosmos/cosmos-sdk/x/supply"
|
"github.com/cosmos/cosmos-sdk/x/supply"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/auction"
|
||||||
|
"github.com/kava-labs/kava/x/bep3"
|
||||||
|
"github.com/kava-labs/kava/x/cdp"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
"github.com/kava-labs/kava/x/incentive"
|
||||||
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
|
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -59,7 +60,7 @@ var (
|
|||||||
staking.AppModuleBasic{},
|
staking.AppModuleBasic{},
|
||||||
mint.AppModuleBasic{},
|
mint.AppModuleBasic{},
|
||||||
distr.AppModuleBasic{},
|
distr.AppModuleBasic{},
|
||||||
gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler),
|
gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler),
|
||||||
params.AppModuleBasic{},
|
params.AppModuleBasic{},
|
||||||
crisis.AppModuleBasic{},
|
crisis.AppModuleBasic{},
|
||||||
slashing.AppModuleBasic{},
|
slashing.AppModuleBasic{},
|
||||||
@ -68,6 +69,7 @@ var (
|
|||||||
auction.AppModuleBasic{},
|
auction.AppModuleBasic{},
|
||||||
cdp.AppModuleBasic{},
|
cdp.AppModuleBasic{},
|
||||||
pricefeed.AppModuleBasic{},
|
pricefeed.AppModuleBasic{},
|
||||||
|
committee.AppModuleBasic{},
|
||||||
bep3.AppModuleBasic{},
|
bep3.AppModuleBasic{},
|
||||||
kavadist.AppModuleBasic{},
|
kavadist.AppModuleBasic{},
|
||||||
incentive.AppModuleBasic{},
|
incentive.AppModuleBasic{},
|
||||||
@ -121,6 +123,7 @@ type App struct {
|
|||||||
auctionKeeper auction.Keeper
|
auctionKeeper auction.Keeper
|
||||||
cdpKeeper cdp.Keeper
|
cdpKeeper cdp.Keeper
|
||||||
pricefeedKeeper pricefeed.Keeper
|
pricefeedKeeper pricefeed.Keeper
|
||||||
|
committeeKeeper committee.Keeper
|
||||||
bep3Keeper bep3.Keeper
|
bep3Keeper bep3.Keeper
|
||||||
kavadistKeeper kavadist.Keeper
|
kavadistKeeper kavadist.Keeper
|
||||||
incentiveKeeper incentive.Keeper
|
incentiveKeeper incentive.Keeper
|
||||||
@ -148,7 +151,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
|
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
|
||||||
gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey,
|
gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey,
|
||||||
auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey,
|
auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey,
|
||||||
kavadist.StoreKey, incentive.StoreKey,
|
kavadist.StoreKey, incentive.StoreKey, committee.StoreKey,
|
||||||
)
|
)
|
||||||
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
|
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
|
||||||
|
|
||||||
@ -245,11 +248,27 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
evidenceKeeper.SetRouter(evidenceRouter)
|
evidenceKeeper.SetRouter(evidenceRouter)
|
||||||
app.evidenceKeeper = *evidenceKeeper
|
app.evidenceKeeper = *evidenceKeeper
|
||||||
|
|
||||||
|
// create committee keeper with router
|
||||||
|
committeeGovRouter := gov.NewRouter()
|
||||||
|
committeeGovRouter.
|
||||||
|
AddRoute(gov.RouterKey, gov.ProposalHandler).
|
||||||
|
AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
|
||||||
|
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper))
|
||||||
|
// Note: the committee proposal handler is not registered on the committee router. This means committees cannot create or update other committees.
|
||||||
|
// Adding the committee proposal handler to the router is possible but awkward as the handler depends on the keeper which depends on the handler.
|
||||||
|
app.committeeKeeper = committee.NewKeeper(
|
||||||
|
app.cdc,
|
||||||
|
keys[committee.StoreKey],
|
||||||
|
committeeGovRouter, // TODO blacklist module addresses?
|
||||||
|
)
|
||||||
|
|
||||||
|
// create gov keeper with router
|
||||||
govRouter := gov.NewRouter()
|
govRouter := gov.NewRouter()
|
||||||
govRouter.
|
govRouter.
|
||||||
AddRoute(gov.RouterKey, gov.ProposalHandler).
|
AddRoute(gov.RouterKey, gov.ProposalHandler).
|
||||||
AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
|
AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
|
||||||
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper))
|
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)).
|
||||||
|
AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper))
|
||||||
app.govKeeper = gov.NewKeeper(
|
app.govKeeper = gov.NewKeeper(
|
||||||
app.cdc,
|
app.cdc,
|
||||||
keys[gov.StoreKey],
|
keys[gov.StoreKey],
|
||||||
@ -334,12 +353,15 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
|
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
|
||||||
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
|
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
|
||||||
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
||||||
|
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
||||||
)
|
)
|
||||||
|
|
||||||
// During begin block slashing happens after distr.BeginBlocker so that
|
// During begin block slashing happens after distr.BeginBlocker so that
|
||||||
// there is nothing left over in the validator fee pool, so as to keep the
|
// there is nothing left over in the validator fee pool, so as to keep the
|
||||||
// CanWithdrawInvariant invariant.
|
// CanWithdrawInvariant invariant.
|
||||||
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, incentive.ModuleName)
|
// Auction.BeginBlocker will close out expired auctions and pay debt back to cdp.
|
||||||
|
// So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions.
|
||||||
|
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, bep3.ModuleName, incentive.ModuleName, committee.ModuleName)
|
||||||
|
|
||||||
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
|
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
|
||||||
|
|
||||||
@ -348,7 +370,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
|||||||
validatorvesting.ModuleName, distr.ModuleName,
|
validatorvesting.ModuleName, distr.ModuleName,
|
||||||
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
||||||
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
|
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
|
||||||
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName,
|
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
|
||||||
|
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName,
|
||||||
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
|
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
|
||||||
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
|
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
|
||||||
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
|
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/kava-labs/kava/x/auction"
|
"github.com/kava-labs/kava/x/auction"
|
||||||
"github.com/kava-labs/kava/x/bep3"
|
"github.com/kava-labs/kava/x/bep3"
|
||||||
"github.com/kava-labs/kava/x/cdp"
|
"github.com/kava-labs/kava/x/cdp"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
"github.com/kava-labs/kava/x/incentive"
|
"github.com/kava-labs/kava/x/incentive"
|
||||||
"github.com/kava-labs/kava/x/kavadist"
|
"github.com/kava-labs/kava/x/kavadist"
|
||||||
"github.com/kava-labs/kava/x/pricefeed"
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
@ -77,6 +78,7 @@ func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefee
|
|||||||
func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
|
func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
|
||||||
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
|
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
|
||||||
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
|
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
|
||||||
|
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
|
||||||
|
|
||||||
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
||||||
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
|
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
|
||||||
|
12
x/committee/abci.go
Normal file
12
x/committee/abci.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package committee
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BeginBlocker runs at the start of every block.
|
||||||
|
func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) {
|
||||||
|
|
||||||
|
k.CloseExpiredProposals(ctx)
|
||||||
|
}
|
70
x/committee/abci_test.go
Normal file
70
x/committee/abci_test.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package committee_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/gov"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModuleTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
keeper committee.Keeper
|
||||||
|
app app.TestApp
|
||||||
|
ctx sdk.Context
|
||||||
|
|
||||||
|
addresses []sdk.AccAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ModuleTestSuite) SetupTest() {
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{})
|
||||||
|
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ModuleTestSuite) TestBeginBlock() {
|
||||||
|
suite.app.InitializeFromGenesisStates()
|
||||||
|
|
||||||
|
normalCom := committee.Committee{
|
||||||
|
ID: 12,
|
||||||
|
Members: suite.addresses[:2],
|
||||||
|
Permissions: []committee.Permission{committee.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.8"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
}
|
||||||
|
suite.keeper.SetCommittee(suite.ctx, normalCom)
|
||||||
|
|
||||||
|
pprop1 := gov.NewTextProposal("1A Title", "A description of this proposal.")
|
||||||
|
id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))
|
||||||
|
pprop2 := gov.NewTextProposal("2A Title", "A description of this proposal.")
|
||||||
|
id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, normalCom.Members[0], normalCom.ID, pprop2)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// Run BeginBlocker
|
||||||
|
proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(normalCom.ProposalDuration))
|
||||||
|
suite.NotPanics(func() {
|
||||||
|
committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check expired proposals are gone
|
||||||
|
_, found := suite.keeper.GetProposal(suite.ctx, id1)
|
||||||
|
suite.False(found, "expected expired proposal to be closed")
|
||||||
|
_, found = suite.keeper.GetProposal(suite.ctx, id2)
|
||||||
|
suite.True(found, "expected non expired proposal to be not closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModuleTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ModuleTestSuite))
|
||||||
|
}
|
101
x/committee/alias.go
Normal file
101
x/committee/alias.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package committee
|
||||||
|
|
||||||
|
// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kava-labs/kava/x/committee/client"
|
||||||
|
"github.com/kava-labs/kava/x/committee/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AttributeKeyCommitteeID = types.AttributeKeyCommitteeID
|
||||||
|
AttributeKeyProposalCloseStatus = types.AttributeKeyProposalCloseStatus
|
||||||
|
AttributeKeyProposalID = types.AttributeKeyProposalID
|
||||||
|
AttributeValueCategory = types.AttributeValueCategory
|
||||||
|
AttributeValueProposalFailed = types.AttributeValueProposalFailed
|
||||||
|
AttributeValueProposalPassed = types.AttributeValueProposalPassed
|
||||||
|
AttributeValueProposalTimeout = types.AttributeValueProposalTimeout
|
||||||
|
DefaultNextProposalID = types.DefaultNextProposalID
|
||||||
|
DefaultParamspace = types.DefaultParamspace
|
||||||
|
EventTypeProposalClose = types.EventTypeProposalClose
|
||||||
|
EventTypeProposalSubmit = types.EventTypeProposalSubmit
|
||||||
|
EventTypeProposalVote = types.EventTypeProposalVote
|
||||||
|
MaxCommitteeDescriptionLength = types.MaxCommitteeDescriptionLength
|
||||||
|
ModuleName = types.ModuleName
|
||||||
|
ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange
|
||||||
|
ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete
|
||||||
|
QuerierRoute = types.QuerierRoute
|
||||||
|
QueryCommittee = types.QueryCommittee
|
||||||
|
QueryCommittees = types.QueryCommittees
|
||||||
|
QueryProposal = types.QueryProposal
|
||||||
|
QueryProposals = types.QueryProposals
|
||||||
|
QueryTally = types.QueryTally
|
||||||
|
QueryVote = types.QueryVote
|
||||||
|
QueryVotes = types.QueryVotes
|
||||||
|
RouterKey = types.RouterKey
|
||||||
|
StoreKey = types.StoreKey
|
||||||
|
TypeMsgSubmitProposal = types.TypeMsgSubmitProposal
|
||||||
|
TypeMsgVote = types.TypeMsgVote
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// function aliases
|
||||||
|
NewKeeper = keeper.NewKeeper
|
||||||
|
NewQuerier = keeper.NewQuerier
|
||||||
|
DefaultGenesisState = types.DefaultGenesisState
|
||||||
|
GetKeyFromID = types.GetKeyFromID
|
||||||
|
GetVoteKey = types.GetVoteKey
|
||||||
|
NewCommittee = types.NewCommittee
|
||||||
|
NewCommitteeChangeProposal = types.NewCommitteeChangeProposal
|
||||||
|
NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal
|
||||||
|
NewGenesisState = types.NewGenesisState
|
||||||
|
NewMsgSubmitProposal = types.NewMsgSubmitProposal
|
||||||
|
NewMsgVote = types.NewMsgVote
|
||||||
|
NewProposal = types.NewProposal
|
||||||
|
NewQueryCommitteeParams = types.NewQueryCommitteeParams
|
||||||
|
NewQueryProposalParams = types.NewQueryProposalParams
|
||||||
|
NewQueryVoteParams = types.NewQueryVoteParams
|
||||||
|
RegisterCodec = types.RegisterCodec
|
||||||
|
RegisterPermissionTypeCodec = types.RegisterPermissionTypeCodec
|
||||||
|
RegisterProposalTypeCodec = types.RegisterProposalTypeCodec
|
||||||
|
Uint64FromBytes = types.Uint64FromBytes
|
||||||
|
|
||||||
|
// variable aliases
|
||||||
|
ProposalHandler = client.ProposalHandler
|
||||||
|
CommitteeKeyPrefix = types.CommitteeKeyPrefix
|
||||||
|
ErrInvalidCommittee = types.ErrInvalidCommittee
|
||||||
|
ErrInvalidGenesis = types.ErrInvalidGenesis
|
||||||
|
ErrInvalidPubProposal = types.ErrInvalidPubProposal
|
||||||
|
ErrNoProposalHandlerExists = types.ErrNoProposalHandlerExists
|
||||||
|
ErrProposalExpired = types.ErrProposalExpired
|
||||||
|
ErrUnknownCommittee = types.ErrUnknownCommittee
|
||||||
|
ErrUnknownProposal = types.ErrUnknownProposal
|
||||||
|
ErrUnknownVote = types.ErrUnknownVote
|
||||||
|
ModuleCdc = types.ModuleCdc
|
||||||
|
NextProposalIDKey = types.NextProposalIDKey
|
||||||
|
ProposalKeyPrefix = types.ProposalKeyPrefix
|
||||||
|
VoteKeyPrefix = types.VoteKeyPrefix
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Keeper = keeper.Keeper
|
||||||
|
AllowedParam = types.AllowedParam
|
||||||
|
AllowedParams = types.AllowedParams
|
||||||
|
Committee = types.Committee
|
||||||
|
CommitteeChangeProposal = types.CommitteeChangeProposal
|
||||||
|
CommitteeDeleteProposal = types.CommitteeDeleteProposal
|
||||||
|
GenesisState = types.GenesisState
|
||||||
|
GodPermission = types.GodPermission
|
||||||
|
MsgSubmitProposal = types.MsgSubmitProposal
|
||||||
|
MsgVote = types.MsgVote
|
||||||
|
ParamChangePermission = types.ParamChangePermission
|
||||||
|
Permission = types.Permission
|
||||||
|
Proposal = types.Proposal
|
||||||
|
PubProposal = types.PubProposal
|
||||||
|
QueryCommitteeParams = types.QueryCommitteeParams
|
||||||
|
QueryProposalParams = types.QueryProposalParams
|
||||||
|
QueryVoteParams = types.QueryVoteParams
|
||||||
|
TextPermission = types.TextPermission
|
||||||
|
Vote = types.Vote
|
||||||
|
)
|
36
x/committee/client/cli/cli_test.go
Normal file
36
x/committee/client/cli/cli_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLITestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
cdc *codec.Codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) SetupTest() {
|
||||||
|
tApp := app.NewTestApp()
|
||||||
|
suite.cdc = tApp.Codec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestExampleCommitteeChangeProposal() {
|
||||||
|
suite.NotPanics(func() { cli.MustGetExampleCommitteeChangeProposal(suite.cdc) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestExampleCommitteeDeleteProposal() {
|
||||||
|
suite.NotPanics(func() { cli.MustGetExampleCommitteeDeleteProposal(suite.cdc) })
|
||||||
|
}
|
||||||
|
func (suite *CLITestSuite) TestExampleParameterChangeProposal() {
|
||||||
|
suite.NotPanics(func() { cli.MustGetExampleParameterChangeProposal(suite.cdc) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLITestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CLITestSuite))
|
||||||
|
}
|
294
x/committee/client/cli/query.go
Normal file
294
x/committee/client/cli/query.go
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client"
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
"github.com/cosmos/cosmos-sdk/version"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/common"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetQueryCmd returns the cli query commands for this module
|
||||||
|
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
queryCmd := &cobra.Command{
|
||||||
|
Use: types.ModuleName,
|
||||||
|
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
|
||||||
|
DisableFlagParsing: true,
|
||||||
|
SuggestionsMinimumDistance: 2,
|
||||||
|
RunE: client.ValidateCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
queryCmd.AddCommand(flags.GetCommands(
|
||||||
|
// committees
|
||||||
|
GetCmdQueryCommittee(queryRoute, cdc),
|
||||||
|
GetCmdQueryCommittees(queryRoute, cdc),
|
||||||
|
// proposals
|
||||||
|
GetCmdQueryProposal(queryRoute, cdc),
|
||||||
|
GetCmdQueryProposals(queryRoute, cdc),
|
||||||
|
// votes
|
||||||
|
GetCmdQueryVotes(queryRoute, cdc),
|
||||||
|
// other
|
||||||
|
GetCmdQueryProposer(queryRoute, cdc),
|
||||||
|
GetCmdQueryTally(queryRoute, cdc))...)
|
||||||
|
|
||||||
|
return queryCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Committees
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// GetCmdQueryCommittee implements a query committee command.
|
||||||
|
func GetCmdQueryCommittee(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "committee [committee-id]",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Query details of a single committee",
|
||||||
|
Example: fmt.Sprintf("%s query %s committee 1", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
committeeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("committee-id %s not a valid uint", args[0])
|
||||||
|
}
|
||||||
|
bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittee), bz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and print result
|
||||||
|
committee := types.Committee{}
|
||||||
|
if err = cdc.UnmarshalJSON(res, &committee); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cliCtx.PrintOutput(committee)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCmdQueryCommittees implements a query committees command.
|
||||||
|
func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "committees",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Short: "Query all committees",
|
||||||
|
Example: fmt.Sprintf("%s query %s committees", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittees), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and print result
|
||||||
|
committees := []types.Committee{} // using empty (not nil) slice so json output returns "[]"" instead of "null" when there's no data
|
||||||
|
if err = cdc.UnmarshalJSON(res, &committees); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cliCtx.PrintOutput(committees)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Proposals
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// GetCmdQueryProposal implements the query proposal command.
|
||||||
|
func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "proposal [proposal-id]",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Query details of a single proposal",
|
||||||
|
Example: fmt.Sprintf("%s query %s proposal 2", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
proposalID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proposal-id %s not a valid uint", args[0])
|
||||||
|
}
|
||||||
|
bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and print results
|
||||||
|
var proposal types.Proposal
|
||||||
|
cdc.MustUnmarshalJSON(res, &proposal)
|
||||||
|
return cliCtx.PrintOutput(proposal)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCmdQueryProposals implements a query proposals command.
|
||||||
|
func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "proposals [committee-id]",
|
||||||
|
Short: "Query all proposals for a committee",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Example: fmt.Sprintf("%s query %s proposals 1", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
committeeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("committee-id %s not a valid uint", args[0])
|
||||||
|
}
|
||||||
|
bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposals), bz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and print results
|
||||||
|
proposals := []types.Proposal{}
|
||||||
|
err = cdc.UnmarshalJSON(res, &proposals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cliCtx.PrintOutput(proposals)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Votes
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// GetCmdQueryVotes implements the command to query for proposal votes.
|
||||||
|
func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "votes [proposal-id]",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Query votes on a proposal",
|
||||||
|
Example: fmt.Sprintf("%s query %s votes 2", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
proposalID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proposal-id %s not a valid int", args[0])
|
||||||
|
}
|
||||||
|
bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryVotes), bz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and print results
|
||||||
|
votes := []types.Vote{} // using empty (not nil) slice so json returns [] instead of null when there's no data
|
||||||
|
err = cdc.UnmarshalJSON(res, &votes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cliCtx.PrintOutput(votes)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Other
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "tally [proposal-id]",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Get the current tally of votes on a proposal",
|
||||||
|
Long: "Query the current tally of votes on a proposal to see the progress of the voting.",
|
||||||
|
Example: fmt.Sprintf("%s query %s tally 2", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
proposalID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proposal-id %s not a valid int", args[0])
|
||||||
|
}
|
||||||
|
bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/tally", queryRoute), bz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and print results
|
||||||
|
var tally bool
|
||||||
|
if err = cdc.UnmarshalJSON(res, &tally); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cliCtx.PrintOutput(tally)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "proposer [proposal-id]",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Query the proposer of a governance proposal",
|
||||||
|
Long: "Query which address proposed a proposal with a given ID.",
|
||||||
|
Example: fmt.Sprintf("%s query %s proposer 2", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// validate that the proposalID is a uint
|
||||||
|
proposalID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proposal-id %s is not a valid uint", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
prop, err := common.QueryProposer(cliCtx, proposalID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cliCtx.PrintOutput(prop)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
240
x/committee/client/cli/tx.go
Normal file
240
x/committee/client/cli/tx.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client"
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/version"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/params"
|
||||||
|
"github.com/tendermint/tendermint/crypto"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
|
||||||
|
txCmd := &cobra.Command{
|
||||||
|
Use: types.ModuleName,
|
||||||
|
Short: "committee governance transactions subcommands",
|
||||||
|
DisableFlagParsing: true,
|
||||||
|
SuggestionsMinimumDistance: 2,
|
||||||
|
RunE: client.ValidateCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
txCmd.AddCommand(flags.PostCommands(
|
||||||
|
GetCmdVote(cdc),
|
||||||
|
GetCmdSubmitProposal(cdc),
|
||||||
|
)...)
|
||||||
|
|
||||||
|
return txCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCmdSubmitProposal returns the command to submit a proposal to a committee
|
||||||
|
func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "submit-proposal [committee-id] [proposal-file]",
|
||||||
|
Short: "Submit a governance proposal to a particular committee",
|
||||||
|
Long: fmt.Sprintf(`Submit a proposal to a committee so they can vote on it.
|
||||||
|
|
||||||
|
The proposal file must be the json encoded forms of the proposal type you want to submit.
|
||||||
|
For example:
|
||||||
|
%s
|
||||||
|
`, MustGetExampleParameterChangeProposal(cdc)),
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||||
|
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Get proposing address
|
||||||
|
proposer := cliCtx.GetFromAddress()
|
||||||
|
|
||||||
|
// Get committee ID
|
||||||
|
committeeID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("committee-id %s not a valid int", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the proposal
|
||||||
|
bz, err := ioutil.ReadFile(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var pubProposal types.PubProposal
|
||||||
|
if err := cdc.UnmarshalJSON(bz, &pubProposal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = pubProposal.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build message and run basic validation
|
||||||
|
msg := types.NewMsgSubmitProposal(pubProposal, proposer, committeeID)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign and broadcast message
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCmdVote returns the command to vote on a proposal.
|
||||||
|
func GetCmdVote(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "vote [proposal-id]",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Short: "Vote for an active proposal",
|
||||||
|
Long: "Submit a yes vote for the proposal with id [proposal-id].",
|
||||||
|
Example: fmt.Sprintf("%s tx %s vote 2", version.ClientName, types.ModuleName),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||||
|
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Get voting address
|
||||||
|
from := cliCtx.GetFromAddress()
|
||||||
|
|
||||||
|
// validate that the proposal id is a uint
|
||||||
|
proposalID, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build vote message and run basic validation
|
||||||
|
msg := types.NewMsgVote(from, proposalID)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGovCmdSubmitProposal returns a command to submit a proposal to the gov module. It is passed to the gov module for use on its command subtree.
|
||||||
|
func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "committee [proposal-file] [deposit]",
|
||||||
|
Short: "Submit a governance proposal to change a committee.",
|
||||||
|
Long: fmt.Sprintf(`Submit a governance proposal to create, alter, or delete a committee.
|
||||||
|
|
||||||
|
The proposal file must be the json encoded form of the proposal type you want to submit.
|
||||||
|
For example, to create or update a committee:
|
||||||
|
%s
|
||||||
|
|
||||||
|
and to delete a committee:
|
||||||
|
%s
|
||||||
|
`, MustGetExampleCommitteeChangeProposal(cdc), MustGetExampleCommitteeDeleteProposal(cdc)),
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||||
|
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
|
||||||
|
// Get proposing address
|
||||||
|
proposer := cliCtx.GetFromAddress()
|
||||||
|
|
||||||
|
// Get the deposit
|
||||||
|
deposit, err := sdk.ParseCoins(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the proposal
|
||||||
|
bz, err := ioutil.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var content govtypes.Content
|
||||||
|
if err := cdc.UnmarshalJSON(bz, &content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = content.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build message and run basic validation
|
||||||
|
msg := govtypes.NewMsgSubmitProposal(content, deposit, proposer)
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign and broadcast message
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal
|
||||||
|
func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
|
||||||
|
exampleChangeProposal := types.NewCommitteeChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
types.NewCommittee(
|
||||||
|
1,
|
||||||
|
"The description of this committee.",
|
||||||
|
[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
|
||||||
|
[]types.Permission{
|
||||||
|
types.ParamChangePermission{
|
||||||
|
AllowedParams: types.AllowedParams{{Subspace: "cdp", Key: "CircuitBreaker"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sdk.MustNewDecFromStr("0.8"),
|
||||||
|
time.Hour*24*7,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
exampleChangeProposalBz, err := cdc.MarshalJSONIndent(exampleChangeProposal, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(exampleChangeProposalBz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal
|
||||||
|
func MustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string {
|
||||||
|
exampleDeleteProposal := types.NewCommitteeDeleteProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
exampleDeleteProposalBz, err := cdc.MarshalJSONIndent(exampleDeleteProposal, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(exampleDeleteProposalBz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetExampleParameterChangeProposal is a helper function to return an example json proposal
|
||||||
|
func MustGetExampleParameterChangeProposal(cdc *codec.Codec) string {
|
||||||
|
exampleParameterChangeProposal := params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", "1000000000")},
|
||||||
|
)
|
||||||
|
exampleParameterChangeProposalBz, err := cdc.MarshalJSONIndent(exampleParameterChangeProposal, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(exampleParameterChangeProposalBz)
|
||||||
|
}
|
60
x/committee/client/common/query_proposer.go
Normal file
60
x/committee/client/common/query_proposer.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: QueryProposer is copied in from the gov module
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPage = 1
|
||||||
|
defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19
|
||||||
|
)
|
||||||
|
|
||||||
|
// Proposer contains metadata of a governance proposal used for querying a proposer.
|
||||||
|
type Proposer struct {
|
||||||
|
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
|
||||||
|
Proposer string `json:"proposer" yaml:"proposer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProposer returns a new Proposer given id and proposer
|
||||||
|
func NewProposer(proposalID uint64, proposer string) Proposer {
|
||||||
|
return Proposer{proposalID, proposer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Proposer) String() string {
|
||||||
|
return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryProposer will query for a proposer of a governance proposal by ID.
|
||||||
|
func QueryProposer(cliCtx context.CLIContext, proposalID uint64) (Proposer, error) {
|
||||||
|
events := []string{
|
||||||
|
fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal),
|
||||||
|
fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))),
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: SearchTxs is used to facilitate the txs query which does not currently
|
||||||
|
// support configurable pagination.
|
||||||
|
searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit)
|
||||||
|
if err != nil {
|
||||||
|
return Proposer{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, info := range searchResult.Txs {
|
||||||
|
for _, msg := range info.Tx.GetMsgs() {
|
||||||
|
// there should only be a single proposal under the given conditions
|
||||||
|
if msg.Type() == types.TypeMsgSubmitProposal {
|
||||||
|
subMsg := msg.(types.MsgSubmitProposal)
|
||||||
|
return NewProposer(proposalID, subMsg.Proposer.String()), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID)
|
||||||
|
}
|
11
x/committee/client/proposal_handler.go
Normal file
11
x/committee/client/proposal_handler.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
govclient "github.com/cosmos/cosmos-sdk/x/gov/client"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/cli"
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProposalHandler is a struct containing handler funcs for submiting CommitteeChange/Delete proposal txs to the gov module through the cli or rest.
|
||||||
|
var ProposalHandler = govclient.NewProposalHandler(cli.GetGovCmdSubmitProposal, rest.ProposalRESTHandler)
|
275
x/committee/client/rest/query.go
Normal file
275
x/committee/client/rest/query.go
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/common"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/committees", types.ModuleName), queryCommitteesHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}", types.ModuleName, RestCommitteeID), queryCommitteeHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), queryProposalsHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}", types.ModuleName, RestProposalID), queryProposalHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/proposer", types.ModuleName, RestProposalID), queryProposerHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/tally", types.ModuleName, RestProposalID), queryTallyOnProposalHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), queryVotesOnProposalHandlerFn(cliCtx)).Methods("GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Committees
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryCommitteesHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryCommittees), nil)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryCommitteeHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestCommitteeID]) == 0 {
|
||||||
|
err := errors.New("committeeID required but not specified")
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryCommittee), bz)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Proposals
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryProposalsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestCommitteeID]) == 0 {
|
||||||
|
err := errors.New("committeeID required but not specified")
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposals), bz)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestProposalID]) == 0 {
|
||||||
|
err := errors.New("proposalID required but not specified")
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposal), bz)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, err := common.QueryProposer(cliCtx, proposalID)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Votes
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestProposalID]) == 0 {
|
||||||
|
err := errors.New(fmt.Sprintf("%s required but not specified", RestProposalID))
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryVotes), bz)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse the query height
|
||||||
|
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare params for querier
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestProposalID]) == 0 {
|
||||||
|
err := errors.New(fmt.Sprintf("%s required but not specified", RestProposalID))
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query
|
||||||
|
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryTally), bz)
|
||||||
|
if err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response
|
||||||
|
cliCtx = cliCtx.WithHeight(height)
|
||||||
|
rest.PostProcessResponse(w, cliCtx, res)
|
||||||
|
}
|
||||||
|
}
|
19
x/committee/client/rest/rest.go
Normal file
19
x/committee/client/rest/rest.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// REST Variable names
|
||||||
|
const (
|
||||||
|
RestProposalID = "proposal-id"
|
||||||
|
RestCommitteeID = "committee-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes - Central function to define routes that get registered by the main application
|
||||||
|
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
|
registerQueryRoutes(cliCtx, r)
|
||||||
|
registerTxRoutes(cliCtx, r)
|
||||||
|
}
|
149
x/committee/client/rest/tx.go
Normal file
149
x/committee/client/rest/tx.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||||
|
govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), postProposalHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), postVoteHandlerFn(cliCtx)).Methods("POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostProposalReq defines the properties of a proposal request's body.
|
||||||
|
type PostProposalReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
PubProposal types.PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
|
||||||
|
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// Parse and validate url params
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestCommitteeID]) == 0 {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestCommitteeID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate http request body
|
||||||
|
var req PostProposalReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.BaseReq = req.BaseReq.Sanitize()
|
||||||
|
if !req.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := req.PubProposal.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return a StdTx
|
||||||
|
msg := types.NewMsgSubmitProposal(req.PubProposal, req.Proposer, committeeID)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostVoteReq defines the properties of a vote request's body.
|
||||||
|
type PostVoteReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// Parse and validate url params
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
if len(vars[RestProposalID]) == 0 {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestProposalID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate http request body
|
||||||
|
var req PostVoteReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.BaseReq = req.BaseReq.Sanitize()
|
||||||
|
if !req.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return a StdTx
|
||||||
|
msg := types.NewMsgVote(req.Voter, proposalID)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a rest handler for for the gov module, that handles committee change/delete proposals.
|
||||||
|
type PostGovProposalReq struct {
|
||||||
|
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||||
|
Content govtypes.Content `json:"content" yaml:"content"`
|
||||||
|
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
|
||||||
|
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler {
|
||||||
|
return govrest.ProposalRESTHandler{
|
||||||
|
SubRoute: "committee",
|
||||||
|
Handler: postGovProposalHandlerFn(cliCtx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postGovProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// Parse and validate http request body
|
||||||
|
var req PostGovProposalReq
|
||||||
|
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.BaseReq = req.BaseReq.Sanitize()
|
||||||
|
if !req.BaseReq.ValidateBasic(w) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := req.Content.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return a StdTx
|
||||||
|
msg := govtypes.NewMsgSubmitProposal(req.Content, req.Deposit, req.Proposer)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||||
|
}
|
||||||
|
}
|
46
x/committee/genesis.go
Normal file
46
x/committee/genesis.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package committee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitGenesis initializes the store state from a genesis state.
|
||||||
|
func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) {
|
||||||
|
if err := gs.Validate(); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
keeper.SetNextProposalID(ctx, gs.NextProposalID)
|
||||||
|
|
||||||
|
for _, com := range gs.Committees {
|
||||||
|
keeper.SetCommittee(ctx, com)
|
||||||
|
}
|
||||||
|
for _, p := range gs.Proposals {
|
||||||
|
keeper.SetProposal(ctx, p)
|
||||||
|
}
|
||||||
|
for _, v := range gs.Votes {
|
||||||
|
keeper.SetVote(ctx, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportGenesis returns a GenesisState for a given context and keeper.
|
||||||
|
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
|
||||||
|
|
||||||
|
nextID, err := keeper.GetNextProposalID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
committees := keeper.GetCommittees(ctx)
|
||||||
|
proposals := keeper.GetProposals(ctx)
|
||||||
|
votes := keeper.GetVotes(ctx)
|
||||||
|
|
||||||
|
return types.NewGenesisState(
|
||||||
|
nextID,
|
||||||
|
committees,
|
||||||
|
proposals,
|
||||||
|
votes,
|
||||||
|
)
|
||||||
|
}
|
75
x/committee/genesis_test.go
Normal file
75
x/committee/genesis_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package committee_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenesisTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
app app.TestApp
|
||||||
|
ctx sdk.Context
|
||||||
|
keeper committee.Keeper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *GenesisTestSuite) TestGenesis() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
genState types.GenesisState
|
||||||
|
expectPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
genState: types.DefaultGenesisState(),
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
genState: types.NewGenesisState(
|
||||||
|
2,
|
||||||
|
[]types.Committee{},
|
||||||
|
[]types.Proposal{{ID: 1, CommitteeID: 57}},
|
||||||
|
[]types.Vote{},
|
||||||
|
),
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
// Setup (note: suite.SetupTest is not run before every suite.Run)
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{})
|
||||||
|
|
||||||
|
// Run
|
||||||
|
var exportedGenState types.GenesisState
|
||||||
|
run := func() {
|
||||||
|
committee.InitGenesis(suite.ctx, suite.keeper, tc.genState)
|
||||||
|
exportedGenState = committee.ExportGenesis(suite.ctx, suite.keeper)
|
||||||
|
}
|
||||||
|
if tc.expectPass {
|
||||||
|
suite.NotPanics(run)
|
||||||
|
} else {
|
||||||
|
suite.Panics(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check
|
||||||
|
if tc.expectPass {
|
||||||
|
suite.Equal(tc.genState, exportedGenState)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenesisTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(GenesisTestSuite))
|
||||||
|
}
|
95
x/committee/handler.go
Normal file
95
x/committee/handler.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package committee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHandler creates an sdk.Handler for committee messages
|
||||||
|
func NewHandler(k keeper.Keeper) sdk.Handler {
|
||||||
|
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
|
||||||
|
ctx = ctx.WithEventManager(sdk.NewEventManager())
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case types.MsgSubmitProposal:
|
||||||
|
return handleMsgSubmitProposal(ctx, k, msg)
|
||||||
|
case types.MsgVote:
|
||||||
|
return handleMsgVote(ctx, k, msg)
|
||||||
|
default:
|
||||||
|
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubmitProposal) (*sdk.Result, error) {
|
||||||
|
proposalID, err := k.SubmitProposal(ctx, msg.Proposer, msg.CommitteeID, msg.PubProposal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
sdk.EventTypeMessage,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &sdk.Result{
|
||||||
|
Data: GetKeyFromID(proposalID),
|
||||||
|
Events: ctx.EventManager().Events(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Result, error) {
|
||||||
|
// get the proposal just to add fields to the event
|
||||||
|
proposal, found := k.GetProposal(ctx, msg.ProposalID)
|
||||||
|
if !found {
|
||||||
|
return nil, sdkerrors.Wrapf(ErrUnknownProposal, "%d", msg.ProposalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := k.AddVote(ctx, msg.ProposalID, msg.Voter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
sdk.EventTypeMessage,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeySender, msg.Voter.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enact a proposal if it has enough votes
|
||||||
|
passes, err := k.GetProposalResult(ctx, msg.ProposalID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !passes {
|
||||||
|
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k.EnactProposal(ctx, msg.ProposalID)
|
||||||
|
outcome := types.AttributeValueProposalPassed
|
||||||
|
if err != nil {
|
||||||
|
outcome = types.AttributeValueProposalFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
k.DeleteProposalAndVotes(ctx, msg.ProposalID)
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeProposalClose,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
|
||||||
|
}
|
210
x/committee/handler_test.go
Normal file
210
x/committee/handler_test.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package committee_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/distribution"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/params"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
"github.com/kava-labs/kava/x/committee/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDistributionGenesisWithPool creates a default distribution genesis state with some coins in the community pool.
|
||||||
|
func NewDistributionGenesisWithPool(communityPoolCoins sdk.Coins) app.GenesisState {
|
||||||
|
gs := distribution.DefaultGenesisState()
|
||||||
|
gs.FeePool = distribution.FeePool{CommunityPool: sdk.NewDecCoinsFromCoins(communityPoolCoins...)}
|
||||||
|
return app.GenesisState{distribution.ModuleName: distribution.ModuleCdc.MustMarshalJSON(gs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandlerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
app app.TestApp
|
||||||
|
keeper keeper.Keeper
|
||||||
|
handler sdk.Handler
|
||||||
|
ctx sdk.Context
|
||||||
|
addresses []sdk.AccAddress
|
||||||
|
|
||||||
|
communityPoolAmt sdk.Coins
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerTestSuite) SetupTest() {
|
||||||
|
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.handler = committee.NewHandler(suite.keeper)
|
||||||
|
|
||||||
|
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
|
||||||
|
testGenesis := types.NewGenesisState(
|
||||||
|
3,
|
||||||
|
[]types.Committee{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses[:3],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.5"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]types.Proposal{},
|
||||||
|
[]types.Vote{},
|
||||||
|
)
|
||||||
|
suite.communityPoolAmt = cs(c("ukava", 1000))
|
||||||
|
suite.app.InitializeFromGenesisStates(
|
||||||
|
NewCommitteeGenesisState(suite.app.Codec(), testGenesis),
|
||||||
|
NewDistributionGenesisWithPool(suite.communityPoolAmt),
|
||||||
|
)
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Valid() {
|
||||||
|
msg := committee.NewMsgSubmitProposal(
|
||||||
|
params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: cdptypes.ModuleName,
|
||||||
|
Key: string(cdptypes.KeyDebtThreshold),
|
||||||
|
Value: string(types.ModuleCdc.MustMarshalJSON(i(1000000))),
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
suite.addresses[0],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
res, err := suite.handler(suite.ctx, msg)
|
||||||
|
|
||||||
|
suite.NoError(err)
|
||||||
|
_, found := suite.keeper.GetProposal(suite.ctx, types.Uint64FromBytes(res.Data))
|
||||||
|
suite.True(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() {
|
||||||
|
var committeeID uint64 = 1
|
||||||
|
msg := types.NewMsgSubmitProposal(
|
||||||
|
params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: cdptypes.ModuleName,
|
||||||
|
Key: "nonsense-key",
|
||||||
|
Value: "nonsense-value",
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
suite.addresses[0],
|
||||||
|
committeeID,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := suite.handler(suite.ctx, msg)
|
||||||
|
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Empty(
|
||||||
|
suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID),
|
||||||
|
"proposal found when none should exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() {
|
||||||
|
var committeeID uint64 = 1
|
||||||
|
msg := types.NewMsgSubmitProposal(
|
||||||
|
UnregisteredPubProposal{},
|
||||||
|
suite.addresses[0],
|
||||||
|
committeeID,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := suite.handler(suite.ctx, msg)
|
||||||
|
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Empty(
|
||||||
|
suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID),
|
||||||
|
"proposal found when none should exist",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() {
|
||||||
|
previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold
|
||||||
|
newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000))
|
||||||
|
msg := types.NewMsgSubmitProposal(
|
||||||
|
params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: cdptypes.ModuleName,
|
||||||
|
Key: string(cdptypes.KeyDebtThreshold),
|
||||||
|
Value: string(types.ModuleCdc.MustMarshalJSON(newDebtThreshold)),
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
suite.addresses[0],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
res, err := suite.handler(suite.ctx, msg)
|
||||||
|
suite.NoError(err)
|
||||||
|
proposalID := types.Uint64FromBytes(res.Data)
|
||||||
|
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID))
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// Add a vote to make the proposal pass
|
||||||
|
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID))
|
||||||
|
|
||||||
|
suite.NoError(err)
|
||||||
|
// Check the param has been updated
|
||||||
|
suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold)
|
||||||
|
// Check proposal and votes are gone
|
||||||
|
_, found := suite.keeper.GetProposal(suite.ctx, proposalID)
|
||||||
|
suite.False(found)
|
||||||
|
suite.Empty(
|
||||||
|
suite.keeper.GetVotesByProposal(suite.ctx, proposalID),
|
||||||
|
"vote found when there should be none",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() {
|
||||||
|
recipient := suite.addresses[4]
|
||||||
|
recipientCoins := suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient)
|
||||||
|
msg := types.NewMsgSubmitProposal(
|
||||||
|
distribution.NewCommunityPoolSpendProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
recipient,
|
||||||
|
cs(c("ukava", 500)),
|
||||||
|
),
|
||||||
|
suite.addresses[0],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
res, err := suite.handler(suite.ctx, msg)
|
||||||
|
suite.NoError(err)
|
||||||
|
proposalID := types.Uint64FromBytes(res.Data)
|
||||||
|
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID))
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
// invalidate the proposal by emptying community pool
|
||||||
|
suite.app.GetDistrKeeper().DistributeFromFeePool(suite.ctx, suite.communityPoolAmt, suite.addresses[0])
|
||||||
|
|
||||||
|
// Add a vote to make the proposal pass
|
||||||
|
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID))
|
||||||
|
|
||||||
|
suite.NoError(err)
|
||||||
|
// Check the proposal was not enacted
|
||||||
|
suite.Equal(recipientCoins, suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient))
|
||||||
|
// Check proposal and votes are gone
|
||||||
|
_, found := suite.keeper.GetProposal(suite.ctx, proposalID)
|
||||||
|
suite.False(found)
|
||||||
|
suite.Empty(
|
||||||
|
suite.keeper.GetVotesByProposal(suite.ctx, proposalID),
|
||||||
|
"vote found when there should be none",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlerTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(HandlerTestSuite))
|
||||||
|
}
|
32
x/committee/integration_test.go
Normal file
32
x/committee/integration_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package committee_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Avoid cluttering test cases with long function names
|
||||||
|
func i(in int64) sdk.Int { return sdk.NewInt(in) }
|
||||||
|
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
|
||||||
|
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
|
||||||
|
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
|
||||||
|
|
||||||
|
// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps.
|
||||||
|
func NewCommitteeGenesisState(cdc *codec.Codec, gs types.GenesisState) app.GenesisState {
|
||||||
|
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(gs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.PubProposal = UnregisteredPubProposal{}
|
||||||
|
|
||||||
|
// UnregisteredPubProposal is a pubproposal type that is not registered on the amino codec.
|
||||||
|
type UnregisteredPubProposal struct{}
|
||||||
|
|
||||||
|
func (UnregisteredPubProposal) GetTitle() string { return "unregistered" }
|
||||||
|
func (UnregisteredPubProposal) GetDescription() string { return "unregistered" }
|
||||||
|
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
|
||||||
|
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
|
||||||
|
func (UnregisteredPubProposal) ValidateBasic() error { return nil }
|
||||||
|
func (UnregisteredPubProposal) String() string { return "unregistered" }
|
34
x/committee/keeper/integration_test.go
Normal file
34
x/committee/keeper/integration_test.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
"github.com/kava-labs/kava/x/committee/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Avoid cluttering test cases with long function names
|
||||||
|
func i(in int64) sdk.Int { return sdk.NewInt(in) }
|
||||||
|
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
|
||||||
|
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
|
||||||
|
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
|
||||||
|
|
||||||
|
// getProposalVoteMap collects up votes into a map indexed by proposalID
|
||||||
|
func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) {
|
||||||
|
|
||||||
|
proposalVoteMap := map[uint64]([]types.Vote){}
|
||||||
|
|
||||||
|
k.IterateProposals(ctx, func(p types.Proposal) bool {
|
||||||
|
proposalVoteMap[p.ID] = k.GetVotesByProposal(ctx, p.ID)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return proposalVoteMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps.
|
||||||
|
func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
|
||||||
|
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
|
||||||
|
}
|
281
x/committee/keeper/keeper.go
Normal file
281
x/committee/keeper/keeper.go
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Keeper struct {
|
||||||
|
cdc *codec.Codec
|
||||||
|
storeKey sdk.StoreKey
|
||||||
|
|
||||||
|
// Proposal router
|
||||||
|
router govtypes.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper {
|
||||||
|
// Logic in the keeper methods assume the set of gov handlers is fixed.
|
||||||
|
// So the gov router must be sealed so no handlers can be added or removed after the keeper is created.
|
||||||
|
router.Seal()
|
||||||
|
|
||||||
|
return Keeper{
|
||||||
|
cdc: cdc,
|
||||||
|
storeKey: storeKey,
|
||||||
|
router: router,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Committees
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// GetCommittee gets a committee from the store.
|
||||||
|
func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
|
||||||
|
bz := store.Get(types.GetKeyFromID(committeeID))
|
||||||
|
if bz == nil {
|
||||||
|
return types.Committee{}, false
|
||||||
|
}
|
||||||
|
var committee types.Committee
|
||||||
|
k.cdc.MustUnmarshalBinaryBare(bz, &committee)
|
||||||
|
return committee, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCommittee puts a committee into the store.
|
||||||
|
func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
|
||||||
|
bz := k.cdc.MustMarshalBinaryBare(committee)
|
||||||
|
store.Set(types.GetKeyFromID(committee.ID), bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCommittee removes a committee from the store.
|
||||||
|
func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
|
||||||
|
store.Delete(types.GetKeyFromID(committeeID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateCommittees provides an iterator over all stored committees.
|
||||||
|
// For each committee, cb will be called. If cb returns true, the iterator will close and stop.
|
||||||
|
func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Committee) (stop bool)) {
|
||||||
|
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
|
||||||
|
|
||||||
|
defer iterator.Close()
|
||||||
|
for ; iterator.Valid(); iterator.Next() {
|
||||||
|
var committee types.Committee
|
||||||
|
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &committee)
|
||||||
|
|
||||||
|
if cb(committee) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommittees returns all stored committees.
|
||||||
|
func (k Keeper) GetCommittees(ctx sdk.Context) []types.Committee {
|
||||||
|
results := []types.Committee{}
|
||||||
|
k.IterateCommittees(ctx, func(com types.Committee) bool {
|
||||||
|
results = append(results, com)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Proposals
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// SetNextProposalID stores an ID to be used for the next created proposal
|
||||||
|
func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) {
|
||||||
|
store := ctx.KVStore(k.storeKey)
|
||||||
|
store.Set(types.NextProposalIDKey, types.GetKeyFromID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextProposalID reads the next available global ID from store
|
||||||
|
func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, error) {
|
||||||
|
store := ctx.KVStore(k.storeKey)
|
||||||
|
bz := store.Get(types.NextProposalIDKey)
|
||||||
|
if bz == nil {
|
||||||
|
return 0, sdkerrors.Wrap(types.ErrInvalidGenesis, "next proposal ID not set at genesis")
|
||||||
|
}
|
||||||
|
return types.Uint64FromBytes(bz), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementNextProposalID increments the next proposal ID in the store by 1.
|
||||||
|
func (k Keeper) IncrementNextProposalID(ctx sdk.Context) error {
|
||||||
|
id, err := k.GetNextProposalID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k.SetNextProposalID(ctx, id+1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreNewProposal stores a proposal, adding a new ID
|
||||||
|
func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, committeeID uint64, deadline time.Time) (uint64, error) {
|
||||||
|
newProposalID, err := k.GetNextProposalID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
proposal := types.NewProposal(
|
||||||
|
pubProposal,
|
||||||
|
newProposalID,
|
||||||
|
committeeID,
|
||||||
|
deadline,
|
||||||
|
)
|
||||||
|
|
||||||
|
k.SetProposal(ctx, proposal)
|
||||||
|
|
||||||
|
err = k.IncrementNextProposalID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return newProposalID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProposal gets a proposal from the store.
|
||||||
|
func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
|
||||||
|
bz := store.Get(types.GetKeyFromID(proposalID))
|
||||||
|
if bz == nil {
|
||||||
|
return types.Proposal{}, false
|
||||||
|
}
|
||||||
|
var proposal types.Proposal
|
||||||
|
k.cdc.MustUnmarshalBinaryBare(bz, &proposal)
|
||||||
|
return proposal, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProposal puts a proposal into the store.
|
||||||
|
func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
|
||||||
|
bz := k.cdc.MustMarshalBinaryBare(proposal)
|
||||||
|
store.Set(types.GetKeyFromID(proposal.ID), bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProposal removes a proposal from the store.
|
||||||
|
func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
|
||||||
|
store.Delete(types.GetKeyFromID(proposalID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateProposals provides an iterator over all stored proposals.
|
||||||
|
// For each proposal, cb will be called. If cb returns true, the iterator will close and stop.
|
||||||
|
func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposal) (stop bool)) {
|
||||||
|
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
|
||||||
|
|
||||||
|
defer iterator.Close()
|
||||||
|
for ; iterator.Valid(); iterator.Next() {
|
||||||
|
var proposal types.Proposal
|
||||||
|
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &proposal)
|
||||||
|
|
||||||
|
if cb(proposal) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProposals returns all stored proposals.
|
||||||
|
func (k Keeper) GetProposals(ctx sdk.Context) []types.Proposal {
|
||||||
|
results := []types.Proposal{}
|
||||||
|
k.IterateProposals(ctx, func(prop types.Proposal) bool {
|
||||||
|
results = append(results, prop)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProposalsByCommittee returns all proposals for one committee.
|
||||||
|
func (k Keeper) GetProposalsByCommittee(ctx sdk.Context, committeeID uint64) []types.Proposal {
|
||||||
|
results := []types.Proposal{}
|
||||||
|
k.IterateProposals(ctx, func(prop types.Proposal) bool {
|
||||||
|
if prop.CommitteeID == committeeID {
|
||||||
|
results = append(results, prop)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProposalAndVotes removes a proposal and its associated votes.
|
||||||
|
func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) {
|
||||||
|
|
||||||
|
votes := k.GetVotesByProposal(ctx, proposalID)
|
||||||
|
|
||||||
|
k.DeleteProposal(ctx, proposalID)
|
||||||
|
for _, v := range votes {
|
||||||
|
k.DeleteVote(ctx, v.ProposalID, v.Voter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Votes
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// GetVote gets a vote from the store.
|
||||||
|
func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
|
||||||
|
bz := store.Get(types.GetVoteKey(proposalID, voter))
|
||||||
|
if bz == nil {
|
||||||
|
return types.Vote{}, false
|
||||||
|
}
|
||||||
|
var vote types.Vote
|
||||||
|
k.cdc.MustUnmarshalBinaryBare(bz, &vote)
|
||||||
|
return vote, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVote puts a vote into the store.
|
||||||
|
func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
|
||||||
|
bz := k.cdc.MustMarshalBinaryBare(vote)
|
||||||
|
store.Set(types.GetVoteKey(vote.ProposalID, vote.Voter), bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteVote removes a Vote from the store.
|
||||||
|
func (k Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) {
|
||||||
|
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
|
||||||
|
store.Delete(types.GetVoteKey(proposalID, voter))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterateVotes provides an iterator over all stored votes.
|
||||||
|
// For each vote, cb will be called. If cb returns true, the iterator will close and stop.
|
||||||
|
func (k Keeper) IterateVotes(ctx sdk.Context, cb func(vote types.Vote) (stop bool)) {
|
||||||
|
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
|
||||||
|
|
||||||
|
defer iterator.Close()
|
||||||
|
for ; iterator.Valid(); iterator.Next() {
|
||||||
|
var vote types.Vote
|
||||||
|
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &vote)
|
||||||
|
|
||||||
|
if cb(vote) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVotes returns all stored votes.
|
||||||
|
func (k Keeper) GetVotes(ctx sdk.Context) []types.Vote {
|
||||||
|
results := []types.Vote{}
|
||||||
|
k.IterateVotes(ctx, func(vote types.Vote) bool {
|
||||||
|
results = append(results, vote)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVotesByProposal returns all votes for one proposal.
|
||||||
|
func (k Keeper) GetVotesByProposal(ctx sdk.Context, proposalID uint64) []types.Vote {
|
||||||
|
results := []types.Vote{}
|
||||||
|
k.IterateVotes(ctx, func(vote types.Vote) bool {
|
||||||
|
if vote.ProposalID == proposalID {
|
||||||
|
results = append(results, vote)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
112
x/committee/keeper/keeper_test.go
Normal file
112
x/committee/keeper/keeper_test.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/gov"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeeperTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
keeper keeper.Keeper
|
||||||
|
app app.TestApp
|
||||||
|
ctx sdk.Context
|
||||||
|
|
||||||
|
addresses []sdk.AccAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) SetupTest() {
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{})
|
||||||
|
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() {
|
||||||
|
// setup test
|
||||||
|
com := types.Committee{
|
||||||
|
ID: 12,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses,
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
// write and read from store
|
||||||
|
suite.keeper.SetCommittee(suite.ctx, com)
|
||||||
|
readCommittee, found := suite.keeper.GetCommittee(suite.ctx, com.ID)
|
||||||
|
|
||||||
|
// check before and after match
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(com, readCommittee)
|
||||||
|
|
||||||
|
// delete from store
|
||||||
|
suite.keeper.DeleteCommittee(suite.ctx, com.ID)
|
||||||
|
|
||||||
|
// check does not exist
|
||||||
|
_, found = suite.keeper.GetCommittee(suite.ctx, com.ID)
|
||||||
|
suite.False(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestGetSetDeleteProposal() {
|
||||||
|
// test setup
|
||||||
|
prop := types.Proposal{
|
||||||
|
ID: 12,
|
||||||
|
CommitteeID: 0,
|
||||||
|
PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
Deadline: time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
|
||||||
|
// write and read from store
|
||||||
|
suite.keeper.SetProposal(suite.ctx, prop)
|
||||||
|
readProposal, found := suite.keeper.GetProposal(suite.ctx, prop.ID)
|
||||||
|
|
||||||
|
// check before and after match
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(prop, readProposal)
|
||||||
|
|
||||||
|
// delete from store
|
||||||
|
suite.keeper.DeleteProposal(suite.ctx, prop.ID)
|
||||||
|
|
||||||
|
// check does not exist
|
||||||
|
_, found = suite.keeper.GetProposal(suite.ctx, prop.ID)
|
||||||
|
suite.False(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestGetSetDeleteVote() {
|
||||||
|
// test setup
|
||||||
|
vote := types.Vote{
|
||||||
|
ProposalID: 12,
|
||||||
|
Voter: suite.addresses[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
// write and read from store
|
||||||
|
suite.keeper.SetVote(suite.ctx, vote)
|
||||||
|
readVote, found := suite.keeper.GetVote(suite.ctx, vote.ProposalID, vote.Voter)
|
||||||
|
|
||||||
|
// check before and after match
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(vote, readVote)
|
||||||
|
|
||||||
|
// delete from store
|
||||||
|
suite.keeper.DeleteVote(suite.ctx, vote.ProposalID, vote.Voter)
|
||||||
|
|
||||||
|
// check does not exist
|
||||||
|
_, found = suite.keeper.GetVote(suite.ctx, vote.ProposalID, vote.Voter)
|
||||||
|
suite.False(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeeperTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(KeeperTestSuite))
|
||||||
|
}
|
179
x/committee/keeper/proposal.go
Normal file
179
x/committee/keeper/proposal.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubmitProposal adds a proposal to a committee so that it can be voted on.
|
||||||
|
func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, error) {
|
||||||
|
// Limit proposals to only be submitted by committee members
|
||||||
|
com, found := k.GetCommittee(ctx, committeeID)
|
||||||
|
if !found {
|
||||||
|
return 0, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", committeeID)
|
||||||
|
}
|
||||||
|
if !com.HasMember(proposer) {
|
||||||
|
return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "proposer not member of committee")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check committee has permissions to enact proposal.
|
||||||
|
if !com.HasPermissionsFor(pubProposal) {
|
||||||
|
return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check proposal is valid
|
||||||
|
if err := k.ValidatePubProposal(ctx, pubProposal); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a new ID and store the proposal
|
||||||
|
deadline := ctx.BlockTime().Add(com.ProposalDuration)
|
||||||
|
proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeProposalSubmit,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return proposalID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddVote submits a vote on a proposal.
|
||||||
|
func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) error {
|
||||||
|
// Validate
|
||||||
|
pr, found := k.GetProposal(ctx, proposalID)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
|
||||||
|
}
|
||||||
|
if pr.HasExpiredBy(ctx.BlockTime()) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrProposalExpired, "%s ≥ %s", ctx.BlockTime(), pr.Deadline)
|
||||||
|
|
||||||
|
}
|
||||||
|
com, found := k.GetCommittee(ctx, pr.CommitteeID)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
|
||||||
|
}
|
||||||
|
if !com.HasMember(voter) {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store vote, overwriting any prior vote
|
||||||
|
k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter})
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeProposalVote,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProposalResult calculates if a proposal currently has enough votes to pass.
|
||||||
|
func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, error) {
|
||||||
|
pr, found := k.GetProposal(ctx, proposalID)
|
||||||
|
if !found {
|
||||||
|
return false, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
|
||||||
|
}
|
||||||
|
com, found := k.GetCommittee(ctx, pr.CommitteeID)
|
||||||
|
if !found {
|
||||||
|
return false, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
numVotes := k.TallyVotes(ctx, proposalID)
|
||||||
|
|
||||||
|
proposalResult := sdk.NewDec(numVotes).GTE(com.VoteThreshold.MulInt64(int64(len(com.Members))))
|
||||||
|
|
||||||
|
return proposalResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TallyVotes counts all the votes on a proposal
|
||||||
|
func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 {
|
||||||
|
|
||||||
|
votes := k.GetVotesByProposal(ctx, proposalID)
|
||||||
|
|
||||||
|
return int64(len(votes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnactProposal makes the changes proposed in a proposal.
|
||||||
|
func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) error {
|
||||||
|
pr, found := k.GetProposal(ctx, proposalID)
|
||||||
|
if !found {
|
||||||
|
return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := k.ValidatePubProposal(ctx, pr.PubProposal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
handler := k.router.GetRoute(pr.ProposalRoute())
|
||||||
|
if err := handler(ctx, pr.PubProposal); err != nil {
|
||||||
|
// the handler should not error as it was checked in ValidatePubProposal
|
||||||
|
panic(fmt.Sprintf("unexpected handler error: %s", err))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseExpiredProposals removes proposals (and associated votes) that have past their deadline.
|
||||||
|
func (k Keeper) CloseExpiredProposals(ctx sdk.Context) {
|
||||||
|
|
||||||
|
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
|
||||||
|
if !proposal.HasExpiredBy(ctx.BlockTime()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
k.DeleteProposalAndVotes(ctx, proposal.ID)
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeProposalClose,
|
||||||
|
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePubProposal checks if a pubproposal is valid.
|
||||||
|
func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) {
|
||||||
|
if pubProposal == nil {
|
||||||
|
return sdkerrors.Wrap(types.ErrInvalidPubProposal, "pub proposal cannot be nil")
|
||||||
|
}
|
||||||
|
if err := pubProposal.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !k.router.HasRoute(pubProposal.ProposalRoute()) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrNoProposalHandlerExists, "%T", pubProposal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent.
|
||||||
|
cacheCtx, _ := ctx.CacheContext()
|
||||||
|
handler := k.router.GetRoute(pubProposal.ProposalRoute())
|
||||||
|
|
||||||
|
// Handle an edge case where a param change proposal causes the proposal handler to panic.
|
||||||
|
// A param change proposal with a registered subspace value but unregistered key value will cause a panic in the param change proposal handler.
|
||||||
|
// This defer will catch panics and return a normal error: `recover()` gets the panic value, then the enclosing function's return value is swapped for an error.
|
||||||
|
// reference: https://stackoverflow.com/questions/33167282/how-to-return-a-value-in-a-go-function-that-panics?noredirect=1&lq=1
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
returnErr = sdkerrors.Wrapf(types.ErrInvalidPubProposal, "proposal handler panicked: %s", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := handler(cacheCtx, pubProposal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
446
x/committee/keeper/proposal_test.go
Normal file
446
x/committee/keeper/proposal_test.go
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/gov"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/params"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestSubmitProposal() {
|
||||||
|
normalCom := types.Committee{
|
||||||
|
ID: 12,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses[:2],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
}
|
||||||
|
noPermissionsCom := normalCom
|
||||||
|
noPermissionsCom.Permissions = []types.Permission{}
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
committee types.Committee
|
||||||
|
pubProposal types.PubProposal
|
||||||
|
proposer sdk.AccAddress
|
||||||
|
committeeID uint64
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
committee: normalCom,
|
||||||
|
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
proposer: normalCom.Members[0],
|
||||||
|
committeeID: normalCom.ID,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid proposal",
|
||||||
|
committee: normalCom,
|
||||||
|
pubProposal: nil,
|
||||||
|
proposer: normalCom.Members[0],
|
||||||
|
committeeID: normalCom.ID,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing committee",
|
||||||
|
// no committee
|
||||||
|
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
proposer: suite.addresses[0],
|
||||||
|
committeeID: 0,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not a member",
|
||||||
|
committee: normalCom,
|
||||||
|
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
proposer: suite.addresses[4],
|
||||||
|
committeeID: normalCom.ID,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not enough permissions",
|
||||||
|
committee: noPermissionsCom,
|
||||||
|
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
proposer: noPermissionsCom.Members[0],
|
||||||
|
committeeID: noPermissionsCom.ID,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
// Create local testApp because suite doesn't run the SetupTest function for subtests
|
||||||
|
tApp := app.NewTestApp()
|
||||||
|
keeper := tApp.GetCommitteeKeeper()
|
||||||
|
ctx := tApp.NewContext(true, abci.Header{})
|
||||||
|
tApp.InitializeFromGenesisStates()
|
||||||
|
// setup committee (if required)
|
||||||
|
if !(reflect.DeepEqual(tc.committee, types.Committee{})) {
|
||||||
|
keeper.SetCommittee(ctx, tc.committee)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal)
|
||||||
|
|
||||||
|
if tc.expectErr {
|
||||||
|
suite.NotNil(err)
|
||||||
|
} else {
|
||||||
|
suite.NoError(err)
|
||||||
|
pr, found := keeper.GetProposal(ctx, id)
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(tc.committeeID, pr.CommitteeID)
|
||||||
|
suite.Equal(ctx.BlockTime().Add(tc.committee.ProposalDuration), pr.Deadline)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestAddVote() {
|
||||||
|
normalCom := types.Committee{
|
||||||
|
ID: 12,
|
||||||
|
Members: suite.addresses[:2],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
}
|
||||||
|
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
proposalID uint64
|
||||||
|
voter sdk.AccAddress
|
||||||
|
voteTime time.Time
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
proposalID: types.DefaultNextProposalID,
|
||||||
|
voter: normalCom.Members[0],
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexistent proposal",
|
||||||
|
proposalID: 9999999,
|
||||||
|
voter: normalCom.Members[0],
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "voter not committee member",
|
||||||
|
proposalID: types.DefaultNextProposalID,
|
||||||
|
voter: suite.addresses[4],
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proposal expired",
|
||||||
|
proposalID: types.DefaultNextProposalID,
|
||||||
|
voter: normalCom.Members[0],
|
||||||
|
voteTime: firstBlockTime.Add(normalCom.ProposalDuration),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
// Create local testApp because suite doesn't run the SetupTest function for subtests
|
||||||
|
tApp := app.NewTestApp()
|
||||||
|
keeper := tApp.GetCommitteeKeeper()
|
||||||
|
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
|
||||||
|
tApp.InitializeFromGenesisStates()
|
||||||
|
|
||||||
|
// setup the committee and proposal
|
||||||
|
keeper.SetCommittee(ctx, normalCom)
|
||||||
|
_, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal."))
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
ctx = ctx.WithBlockTime(tc.voteTime)
|
||||||
|
err = keeper.AddVote(ctx, tc.proposalID, tc.voter)
|
||||||
|
|
||||||
|
if tc.expectErr {
|
||||||
|
suite.NotNil(err)
|
||||||
|
} else {
|
||||||
|
suite.NoError(err)
|
||||||
|
_, found := keeper.GetVote(ctx, tc.proposalID, tc.voter)
|
||||||
|
suite.True(found)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestGetProposalResult() {
|
||||||
|
normalCom := types.Committee{
|
||||||
|
ID: 12,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses[:5],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
}
|
||||||
|
var defaultID uint64 = 1
|
||||||
|
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
committee types.Committee
|
||||||
|
votes []types.Vote
|
||||||
|
proposalPasses bool
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "enough votes",
|
||||||
|
committee: normalCom,
|
||||||
|
votes: []types.Vote{
|
||||||
|
{ProposalID: defaultID, Voter: suite.addresses[0]},
|
||||||
|
{ProposalID: defaultID, Voter: suite.addresses[1]},
|
||||||
|
{ProposalID: defaultID, Voter: suite.addresses[2]},
|
||||||
|
{ProposalID: defaultID, Voter: suite.addresses[3]},
|
||||||
|
},
|
||||||
|
proposalPasses: true,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not enough votes",
|
||||||
|
committee: normalCom,
|
||||||
|
votes: []types.Vote{
|
||||||
|
{ProposalID: defaultID, Voter: suite.addresses[0]},
|
||||||
|
},
|
||||||
|
proposalPasses: false,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
// Create local testApp because suite doesn't run the SetupTest function for subtests
|
||||||
|
tApp := app.NewTestApp()
|
||||||
|
keeper := tApp.GetCommitteeKeeper()
|
||||||
|
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
|
||||||
|
|
||||||
|
tApp.InitializeFromGenesisStates(
|
||||||
|
committeeGenState(
|
||||||
|
tApp.Codec(),
|
||||||
|
[]types.Committee{tc.committee},
|
||||||
|
[]types.Proposal{{
|
||||||
|
PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
ID: defaultID,
|
||||||
|
CommitteeID: tc.committee.ID,
|
||||||
|
Deadline: firstBlockTime.Add(time.Hour * 24 * 7),
|
||||||
|
}},
|
||||||
|
tc.votes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
proposalPasses, err := keeper.GetProposalResult(ctx, defaultID)
|
||||||
|
|
||||||
|
if tc.expectErr {
|
||||||
|
suite.NotNil(err)
|
||||||
|
} else {
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(tc.proposalPasses, proposalPasses)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func committeeGenState(cdc *codec.Codec, committees []types.Committee, proposals []types.Proposal, votes []types.Vote) app.GenesisState {
|
||||||
|
gs := types.NewGenesisState(
|
||||||
|
uint64(len(proposals)+1),
|
||||||
|
committees,
|
||||||
|
proposals,
|
||||||
|
votes,
|
||||||
|
)
|
||||||
|
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnregisteredPubProposal struct {
|
||||||
|
gov.TextProposal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
|
||||||
|
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
|
||||||
|
|
||||||
|
var _ types.PubProposal = UnregisteredPubProposal{}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestValidatePubProposal() {
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
pubProposal types.PubProposal
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid (text proposal)",
|
||||||
|
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid (param change proposal)",
|
||||||
|
pubProposal: params.NewParameterChangeProposal(
|
||||||
|
"Change the debt limit",
|
||||||
|
"This proposal changes the debt limit of the cdp module.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: cdptypes.ModuleName,
|
||||||
|
Key: string(cdptypes.KeyGlobalDebtLimit),
|
||||||
|
Value: string(types.ModuleCdc.MustMarshalJSON(cs(c("usdx", 100000000000)))),
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid (missing title)",
|
||||||
|
pubProposal: gov.TextProposal{Description: "A description of this proposal."},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid (unregistered)",
|
||||||
|
pubProposal: UnregisteredPubProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid (nil)",
|
||||||
|
pubProposal: nil,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid (proposal handler fails)",
|
||||||
|
pubProposal: params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: "nonsense-subspace",
|
||||||
|
Key: "nonsense-key",
|
||||||
|
Value: "nonsense-value",
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid (proposal handler panics)",
|
||||||
|
pubProposal: params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: cdptypes.ModuleName,
|
||||||
|
Key: "nonsense-key", // a valid Subspace but invalid Key will trigger a panic in the paramchange propsal handler
|
||||||
|
Value: "nonsense-value",
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid (proposal handler fails - invalid json)",
|
||||||
|
pubProposal: params.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]params.ParamChange{{
|
||||||
|
Subspace: cdptypes.ModuleName,
|
||||||
|
Key: string(cdptypes.KeyGlobalDebtLimit),
|
||||||
|
Value: `{"denom": "usdx",`,
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal)
|
||||||
|
if tc.expectErr {
|
||||||
|
suite.NotNil(err)
|
||||||
|
} else {
|
||||||
|
suite.NoError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestCloseExpiredProposals() {
|
||||||
|
|
||||||
|
// Setup test state
|
||||||
|
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
|
||||||
|
testGenesis := types.NewGenesisState(
|
||||||
|
3,
|
||||||
|
[]types.Committee{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses[:3],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Members: suite.addresses[2:],
|
||||||
|
Permissions: nil,
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]types.Proposal{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
CommitteeID: 1,
|
||||||
|
PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
Deadline: firstBlockTime.Add(7 * 24 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
CommitteeID: 1,
|
||||||
|
PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."),
|
||||||
|
Deadline: firstBlockTime.Add(21 * 24 * time.Hour),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]types.Vote{
|
||||||
|
{ProposalID: 1, Voter: suite.addresses[0]},
|
||||||
|
{ProposalID: 1, Voter: suite.addresses[1]},
|
||||||
|
{ProposalID: 2, Voter: suite.addresses[2]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
suite.app.InitializeFromGenesisStates(
|
||||||
|
NewCommitteeGenesisState(suite.app.Codec(), testGenesis),
|
||||||
|
)
|
||||||
|
|
||||||
|
// close proposals
|
||||||
|
ctx := suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
|
||||||
|
suite.keeper.CloseExpiredProposals(ctx)
|
||||||
|
|
||||||
|
// check
|
||||||
|
for _, p := range testGenesis.Proposals {
|
||||||
|
_, found := suite.keeper.GetProposal(ctx, p.ID)
|
||||||
|
votes := getProposalVoteMap(suite.keeper, ctx)
|
||||||
|
|
||||||
|
if ctx.BlockTime().After(p.Deadline) {
|
||||||
|
suite.False(found)
|
||||||
|
suite.Empty(votes[p.ID])
|
||||||
|
} else {
|
||||||
|
suite.True(found)
|
||||||
|
suite.NotEmpty(votes[p.ID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close (later time)
|
||||||
|
ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime.Add(7 * 24 * time.Hour)})
|
||||||
|
suite.keeper.CloseExpiredProposals(ctx)
|
||||||
|
|
||||||
|
// check
|
||||||
|
for _, p := range testGenesis.Proposals {
|
||||||
|
_, found := suite.keeper.GetProposal(ctx, p.ID)
|
||||||
|
votes := getProposalVoteMap(suite.keeper, ctx)
|
||||||
|
|
||||||
|
if ctx.BlockTime().Equal(p.Deadline) || ctx.BlockTime().After(p.Deadline) {
|
||||||
|
suite.False(found)
|
||||||
|
suite.Empty(votes[p.ID])
|
||||||
|
} else {
|
||||||
|
suite.True(found)
|
||||||
|
suite.NotEmpty(votes[p.ID])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
173
x/committee/keeper/querier.go
Normal file
173
x/committee/keeper/querier.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewQuerier creates a new gov Querier instance
|
||||||
|
func NewQuerier(keeper Keeper) sdk.Querier {
|
||||||
|
return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) {
|
||||||
|
switch path[0] {
|
||||||
|
|
||||||
|
case types.QueryCommittees:
|
||||||
|
return queryCommittees(ctx, path[1:], req, keeper)
|
||||||
|
case types.QueryCommittee:
|
||||||
|
return queryCommittee(ctx, path[1:], req, keeper)
|
||||||
|
case types.QueryProposals:
|
||||||
|
return queryProposals(ctx, path[1:], req, keeper)
|
||||||
|
case types.QueryProposal:
|
||||||
|
return queryProposal(ctx, path[1:], req, keeper)
|
||||||
|
case types.QueryVotes:
|
||||||
|
return queryVotes(ctx, path[1:], req, keeper)
|
||||||
|
case types.QueryVote:
|
||||||
|
return queryVote(ctx, path[1:], req, keeper)
|
||||||
|
case types.QueryTally:
|
||||||
|
return queryTally(ctx, path[1:], req, keeper)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Committees
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
|
||||||
|
committees := keeper.GetCommittees(ctx)
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, committees)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
var params types.QueryCommitteeParams
|
||||||
|
err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
committee, found := keeper.GetCommittee(ctx, params.CommitteeID)
|
||||||
|
if !found {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", params.CommitteeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, committee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Proposals
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
var params types.QueryCommitteeParams
|
||||||
|
err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
proposals := keeper.GetProposalsByCommittee(ctx, params.CommitteeID)
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, proposals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
var params types.QueryProposalParams
|
||||||
|
err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
proposal, found := keeper.GetProposal(ctx, params.ProposalID)
|
||||||
|
if !found {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, proposal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Votes
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
var params types.QueryProposalParams
|
||||||
|
err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
votes := keeper.GetVotesByProposal(ctx, params.ProposalID)
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, votes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
var params types.QueryVoteParams
|
||||||
|
err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
vote, found := keeper.GetVote(ctx, params.ProposalID, params.Voter)
|
||||||
|
if !found {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrUnknownVote, "proposal id: %d, voter: %s", params.ProposalID, params.Voter)
|
||||||
|
}
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, vote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Tally
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
|
||||||
|
var params types.QueryProposalParams
|
||||||
|
err := keeper.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, found := keeper.GetProposal(ctx, params.ProposalID)
|
||||||
|
if !found {
|
||||||
|
return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID)
|
||||||
|
}
|
||||||
|
numVotes := keeper.TallyVotes(ctx, params.ProposalID)
|
||||||
|
|
||||||
|
bz, err := codec.MarshalJSONIndent(keeper.cdc, numVotes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||||
|
}
|
||||||
|
return bz, nil
|
||||||
|
}
|
242
x/committee/keeper/querier_test.go
Normal file
242
x/committee/keeper/querier_test.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
package keeper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/gov"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee/keeper"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
custom = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
type QuerierTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
keeper keeper.Keeper
|
||||||
|
app app.TestApp
|
||||||
|
ctx sdk.Context
|
||||||
|
cdc *codec.Codec
|
||||||
|
|
||||||
|
querier sdk.Querier
|
||||||
|
|
||||||
|
addresses []sdk.AccAddress
|
||||||
|
testGenesis types.GenesisState
|
||||||
|
votes map[uint64]([]types.Vote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) SetupTest() {
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{})
|
||||||
|
suite.cdc = suite.app.Codec()
|
||||||
|
suite.querier = keeper.NewQuerier(suite.keeper)
|
||||||
|
|
||||||
|
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
suite.testGenesis = types.NewGenesisState(
|
||||||
|
3,
|
||||||
|
[]types.Committee{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses[:3],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Members: suite.addresses[2:],
|
||||||
|
Permissions: nil,
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]types.Proposal{
|
||||||
|
{ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
|
||||||
|
{ID: 2, CommitteeID: 1, PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."), Deadline: testTime.Add(21 * 24 * time.Hour)},
|
||||||
|
},
|
||||||
|
[]types.Vote{
|
||||||
|
{ProposalID: 1, Voter: suite.addresses[0]},
|
||||||
|
{ProposalID: 1, Voter: suite.addresses[1]},
|
||||||
|
{ProposalID: 2, Voter: suite.addresses[2]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
suite.app.InitializeFromGenesisStates(
|
||||||
|
NewCommitteeGenesisState(suite.cdc, suite.testGenesis),
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.votes = getProposalVoteMap(suite.keeper, suite.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryCommittees() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false)
|
||||||
|
// Set up request query
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittees}, "/"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryCommittees}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var committees []types.Committee
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
suite.Equal(suite.testGenesis.Committees, committees)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryCommittee() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false) // ?
|
||||||
|
// Set up request query
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"),
|
||||||
|
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].ID)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryCommittee}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var committee types.Committee
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &committee))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
suite.Equal(suite.testGenesis.Committees[0], committee)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryProposals() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false)
|
||||||
|
// Set up request query
|
||||||
|
comID := suite.testGenesis.Proposals[0].CommitteeID
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposals}, "/"),
|
||||||
|
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(comID)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryProposals}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var proposals []types.Proposal
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposals))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
expectedProposals := []types.Proposal{}
|
||||||
|
for _, p := range suite.testGenesis.Proposals {
|
||||||
|
if p.CommitteeID == comID {
|
||||||
|
expectedProposals = append(expectedProposals, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suite.Equal(expectedProposals, proposals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryProposal() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false) // ?
|
||||||
|
// Set up request query
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"),
|
||||||
|
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(suite.testGenesis.Proposals[0].ID)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryProposal}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var proposal types.Proposal
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposal))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
suite.Equal(suite.testGenesis.Proposals[0], proposal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryVotes() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false)
|
||||||
|
// Set up request query
|
||||||
|
propID := suite.testGenesis.Proposals[0].ID
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVotes}, "/"),
|
||||||
|
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryVotes}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var votes []types.Vote
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &votes))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
suite.Equal(suite.votes[propID], votes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryVote() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false) // ?
|
||||||
|
// Set up request query
|
||||||
|
propID := suite.testGenesis.Proposals[0].ID
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVote}, "/"),
|
||||||
|
Data: suite.cdc.MustMarshalJSON(types.NewQueryVoteParams(propID, suite.votes[propID][0].Voter)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryVote}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var vote types.Vote
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &vote))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
suite.Equal(suite.votes[propID][0], vote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QuerierTestSuite) TestQueryTally() {
|
||||||
|
ctx := suite.ctx.WithIsCheckTx(false) // ?
|
||||||
|
// Set up request query
|
||||||
|
propID := suite.testGenesis.Proposals[0].ID
|
||||||
|
query := abci.RequestQuery{
|
||||||
|
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"),
|
||||||
|
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute query and check the []byte result
|
||||||
|
bz, err := suite.querier(ctx, []string{types.QueryTally}, query)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bz)
|
||||||
|
|
||||||
|
// Unmarshal the bytes
|
||||||
|
var tally int64
|
||||||
|
suite.NoError(suite.cdc.UnmarshalJSON(bz, &tally))
|
||||||
|
|
||||||
|
// Check
|
||||||
|
suite.Equal(int64(len(suite.votes[propID])), tally)
|
||||||
|
}
|
||||||
|
func TestQuerierTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(QuerierTestSuite))
|
||||||
|
}
|
169
x/committee/module.go
Normal file
169
x/committee/module.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package committee
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/types/module"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||||
|
sim "github.com/cosmos/cosmos-sdk/x/simulation"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/cli"
|
||||||
|
"github.com/kava-labs/kava/x/committee/client/rest"
|
||||||
|
"github.com/kava-labs/kava/x/committee/simulation"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ module.AppModule = AppModule{}
|
||||||
|
_ module.AppModuleBasic = AppModuleBasic{}
|
||||||
|
_ module.AppModuleSimulation = AppModule{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppModuleBasic app module basics object
|
||||||
|
type AppModuleBasic struct{}
|
||||||
|
|
||||||
|
// Name gets the module name
|
||||||
|
func (AppModuleBasic) Name() string {
|
||||||
|
return ModuleName
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCodec register module codec
|
||||||
|
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
|
||||||
|
RegisterCodec(cdc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultGenesis default genesis state
|
||||||
|
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
|
||||||
|
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateGenesis module validate genesis
|
||||||
|
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
|
||||||
|
var gs GenesisState
|
||||||
|
err := ModuleCdc.UnmarshalJSON(bz, &gs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gs.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRESTRoutes registers the REST routes for the module.
|
||||||
|
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
|
||||||
|
rest.RegisterRoutes(ctx, rtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTxCmd returns the root tx command for the module.
|
||||||
|
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return cli.GetTxCmd(StoreKey, cdc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryCmd returns the root query command for the module.
|
||||||
|
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return cli.GetQueryCmd(StoreKey, cdc)
|
||||||
|
}
|
||||||
|
|
||||||
|
//____________________________________________________________________________
|
||||||
|
|
||||||
|
// AppModule app module type
|
||||||
|
type AppModule struct {
|
||||||
|
AppModuleBasic
|
||||||
|
|
||||||
|
keeper Keeper
|
||||||
|
accountKeeper auth.AccountKeeper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppModule creates a new AppModule object
|
||||||
|
func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper) AppModule {
|
||||||
|
return AppModule{
|
||||||
|
AppModuleBasic: AppModuleBasic{},
|
||||||
|
keeper: keeper,
|
||||||
|
accountKeeper: accountKeeper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name module name
|
||||||
|
func (AppModule) Name() string {
|
||||||
|
return ModuleName
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterInvariants register module invariants
|
||||||
|
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
|
||||||
|
|
||||||
|
// Route module message route name
|
||||||
|
func (AppModule) Route() string {
|
||||||
|
return RouterKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler module handler
|
||||||
|
func (am AppModule) NewHandler() sdk.Handler {
|
||||||
|
return NewHandler(am.keeper)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuerierRoute module querier route name
|
||||||
|
func (AppModule) QuerierRoute() string {
|
||||||
|
return QuerierRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQuerierHandler module querier
|
||||||
|
func (am AppModule) NewQuerierHandler() sdk.Querier {
|
||||||
|
return NewQuerier(am.keeper)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitGenesis module init-genesis
|
||||||
|
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
|
||||||
|
var genesisState GenesisState
|
||||||
|
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
|
||||||
|
InitGenesis(ctx, am.keeper, genesisState)
|
||||||
|
|
||||||
|
return []abci.ValidatorUpdate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportGenesis module export genesis
|
||||||
|
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
|
||||||
|
gs := ExportGenesis(ctx, am.keeper)
|
||||||
|
return ModuleCdc.MustMarshalJSON(gs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginBlock module begin-block
|
||||||
|
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
|
||||||
|
BeginBlocker(ctx, req, am.keeper)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndBlock module end-block
|
||||||
|
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||||
|
return []abci.ValidatorUpdate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//____________________________________________________________________________
|
||||||
|
|
||||||
|
// GenerateGenesisState creates a randomized GenState for the module
|
||||||
|
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
|
||||||
|
simulation.RandomizedGenState(simState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomizedParams returns functions that generate params for the module.
|
||||||
|
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterStoreDecoder registers a decoder for the module's types
|
||||||
|
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
|
||||||
|
sdr[StoreKey] = simulation.DecodeStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeightedOperations returns the all the auction module operations with their respective weights.
|
||||||
|
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
|
||||||
|
return nil // TODO simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
|
||||||
|
}
|
58
x/committee/proposal_handler.go
Normal file
58
x/committee/proposal_handler.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package committee
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewProposalHandler(k Keeper) govtypes.Handler {
|
||||||
|
return func(ctx sdk.Context, content govtypes.Content) error {
|
||||||
|
switch c := content.(type) {
|
||||||
|
case CommitteeChangeProposal:
|
||||||
|
return handleCommitteeChangeProposal(ctx, k, c)
|
||||||
|
case CommitteeDeleteProposal:
|
||||||
|
return handleCommitteeDeleteProposal(ctx, k, c)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s proposal content type: %T", ModuleName, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) error {
|
||||||
|
if err := committeeProposal.ValidateBasic(); err != nil {
|
||||||
|
return sdkerrors.Wrap(ErrInvalidPubProposal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all committee's ongoing proposals
|
||||||
|
k.IterateProposals(ctx, func(p Proposal) bool {
|
||||||
|
if p.CommitteeID != committeeProposal.NewCommittee.ID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
k.DeleteProposalAndVotes(ctx, p.ID)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// update/create the committee
|
||||||
|
k.SetCommittee(ctx, committeeProposal.NewCommittee)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) error {
|
||||||
|
if err := committeeProposal.ValidateBasic(); err != nil {
|
||||||
|
return sdkerrors.Wrap(ErrInvalidPubProposal, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all committee's ongoing proposals
|
||||||
|
k.IterateProposals(ctx, func(p Proposal) bool {
|
||||||
|
if p.CommitteeID != committeeProposal.CommitteeID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
k.DeleteProposalAndVotes(ctx, p.ID)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
k.DeleteCommittee(ctx, committeeProposal.CommitteeID)
|
||||||
|
return nil
|
||||||
|
}
|
225
x/committee/proposal_handler_test.go
Normal file
225
x/committee/proposal_handler_test.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
package committee_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/gov"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/app"
|
||||||
|
"github.com/kava-labs/kava/x/committee"
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
func NewCommitteeGenState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
|
||||||
|
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProposalHandlerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
keeper committee.Keeper
|
||||||
|
app app.TestApp
|
||||||
|
ctx sdk.Context
|
||||||
|
|
||||||
|
addresses []sdk.AccAddress
|
||||||
|
testGenesis committee.GenesisState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ProposalHandlerTestSuite) SetupTest() {
|
||||||
|
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
|
||||||
|
suite.testGenesis = committee.NewGenesisState(
|
||||||
|
2,
|
||||||
|
[]committee.Committee{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: suite.addresses[:3],
|
||||||
|
Permissions: []types.Permission{types.GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Members: suite.addresses[2:],
|
||||||
|
Permissions: nil,
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]committee.Proposal{
|
||||||
|
{ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
|
||||||
|
},
|
||||||
|
[]committee.Vote{
|
||||||
|
{ProposalID: 1, Voter: suite.addresses[0]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
proposal committee.CommitteeChangeProposal
|
||||||
|
expectPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add new",
|
||||||
|
proposal: committee.NewCommitteeChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A proposal description.",
|
||||||
|
committee.Committee{
|
||||||
|
ID: 34,
|
||||||
|
Members: suite.addresses[:1],
|
||||||
|
VoteThreshold: d("1"),
|
||||||
|
ProposalDuration: time.Hour * 24,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update",
|
||||||
|
proposal: committee.NewCommitteeChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A proposal description.",
|
||||||
|
committee.Committee{
|
||||||
|
ID: suite.testGenesis.Committees[0].ID,
|
||||||
|
Members: suite.addresses, // add new members
|
||||||
|
Permissions: suite.testGenesis.Committees[0].Permissions,
|
||||||
|
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold,
|
||||||
|
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid title",
|
||||||
|
proposal: committee.NewCommitteeChangeProposal(
|
||||||
|
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fullfill The Roll Of An Acceptable Governance Proposal Title That Should Succinctly Communicate The Goal And Contents Of The Proposed Proposal To All Parties Involved",
|
||||||
|
"A proposal description.",
|
||||||
|
suite.testGenesis.Committees[0],
|
||||||
|
),
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid committee",
|
||||||
|
proposal: committee.NewCommitteeChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A proposal description.",
|
||||||
|
committee.Committee{
|
||||||
|
ID: suite.testGenesis.Committees[0].ID,
|
||||||
|
Members: append(suite.addresses, suite.addresses[0]), // duplicate address
|
||||||
|
Permissions: suite.testGenesis.Committees[0].Permissions,
|
||||||
|
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold,
|
||||||
|
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
// Setup
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.app = suite.app.InitializeFromGenesisStates(
|
||||||
|
NewCommitteeGenState(suite.app.Codec(), suite.testGenesis),
|
||||||
|
)
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
|
||||||
|
handler := committee.NewProposalHandler(suite.keeper)
|
||||||
|
|
||||||
|
oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID)
|
||||||
|
|
||||||
|
// Run
|
||||||
|
err := handler(suite.ctx, tc.proposal)
|
||||||
|
|
||||||
|
// Check
|
||||||
|
if tc.expectPass {
|
||||||
|
suite.NoError(err)
|
||||||
|
// check committee is accurate
|
||||||
|
actualCom, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.NewCommittee.ID)
|
||||||
|
suite.True(found)
|
||||||
|
suite.Equal(tc.proposal.NewCommittee, actualCom)
|
||||||
|
|
||||||
|
// check proposals and votes for this committee have been removed
|
||||||
|
suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID))
|
||||||
|
for _, p := range oldProposals {
|
||||||
|
suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Equal(suite.testGenesis, committee.ExportGenesis(suite.ctx, suite.keeper))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
proposal committee.CommitteeDeleteProposal
|
||||||
|
expectPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
proposal: committee.NewCommitteeDeleteProposal(
|
||||||
|
"A Title",
|
||||||
|
"A proposal description.",
|
||||||
|
suite.testGenesis.Committees[0].ID,
|
||||||
|
),
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid title",
|
||||||
|
proposal: committee.NewCommitteeDeleteProposal(
|
||||||
|
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fullfill The Roll Of An Acceptable Governance Proposal Title That Should Succinctly Communicate The Goal And Contents Of The Proposed Proposal To All Parties Involved",
|
||||||
|
"A proposal description.",
|
||||||
|
suite.testGenesis.Committees[1].ID,
|
||||||
|
),
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
// Setup
|
||||||
|
suite.app = app.NewTestApp()
|
||||||
|
suite.keeper = suite.app.GetCommitteeKeeper()
|
||||||
|
suite.app = suite.app.InitializeFromGenesisStates(
|
||||||
|
NewCommitteeGenState(suite.app.Codec(), suite.testGenesis),
|
||||||
|
)
|
||||||
|
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
|
||||||
|
handler := committee.NewProposalHandler(suite.keeper)
|
||||||
|
|
||||||
|
oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.CommitteeID)
|
||||||
|
|
||||||
|
// Run
|
||||||
|
err := handler(suite.ctx, tc.proposal)
|
||||||
|
|
||||||
|
// Check
|
||||||
|
if tc.expectPass {
|
||||||
|
suite.NoError(err)
|
||||||
|
// check committee has been removed
|
||||||
|
_, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.CommitteeID)
|
||||||
|
suite.False(found)
|
||||||
|
|
||||||
|
// check proposals and votes for this committee have been removed
|
||||||
|
suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.CommitteeID))
|
||||||
|
for _, p := range oldProposals {
|
||||||
|
suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Equal(suite.testGenesis, committee.ExportGenesis(suite.ctx, suite.keeper))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProposalHandlerTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ProposalHandlerTestSuite))
|
||||||
|
}
|
12
x/committee/simulation/decoder.go
Normal file
12
x/committee/simulation/decoder.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
"github.com/tendermint/tendermint/libs/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeStore unmarshals the KVPair's Value to the corresponding module type
|
||||||
|
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
|
||||||
|
// TODO implement this
|
||||||
|
return ""
|
||||||
|
}
|
22
x/committee/simulation/genesis.go
Normal file
22
x/committee/simulation/genesis.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
"github.com/cosmos/cosmos-sdk/types/module"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/committee/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomizedGenState generates a random GenesisState for the module
|
||||||
|
func RandomizedGenState(simState *module.SimulationState) {
|
||||||
|
|
||||||
|
// TODO implement this fully
|
||||||
|
// - randomly generating the genesis params
|
||||||
|
// - overwriting with genesis provided to simulation
|
||||||
|
genesisState := types.DefaultGenesisState()
|
||||||
|
|
||||||
|
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState))
|
||||||
|
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState)
|
||||||
|
}
|
14
x/committee/simulation/params.go
Normal file
14
x/committee/simulation/params.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package simulation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParamChanges defines the parameters that can be modified by param change proposals
|
||||||
|
// on the simulation
|
||||||
|
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
|
||||||
|
// TODO implement this
|
||||||
|
return []simulation.ParamChange{}
|
||||||
|
}
|
26
x/committee/spec/README.md
Normal file
26
x/committee/spec/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
# `committee`
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `x/committee` module is an additional governance module to `cosmos-sdk/x/gov`.
|
||||||
|
|
||||||
|
It allows groups of accounts to vote on and enact proposals without a full chain governance vote. Certain proposal types can then be decided on quickly in emergency situations, or low risk parameter updates can be delegated to a smaller group of individuals.
|
||||||
|
|
||||||
|
Committees work with "proposals", using the same type from the `gov` module so they are compatible with all existing proposal types such as param changes, or community pool spend, or text proposals.
|
||||||
|
|
||||||
|
Committees have members and permissions.
|
||||||
|
|
||||||
|
Members vote on proposals, with just simple one vote per member, no deposits or slashing. More sophisticated voting could be added.
|
||||||
|
|
||||||
|
Permissions scope the allowed set of proposals a committee can enact. For example:
|
||||||
|
|
||||||
|
- allow the committee to only change the cdp `CircuitBreaker` param.
|
||||||
|
- allow the committee to change auction bid increments, but only within the range [0, 0.1]
|
||||||
|
- allow the committee to only disable cdp msg types, but not staking or gov
|
||||||
|
|
||||||
|
A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be any type with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow.
|
||||||
|
|
||||||
|
This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a committee to only change a particular param, or only change params within a certain percentage.
|
56
x/committee/types/codec.go
Normal file
56
x/committee/types/codec.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
|
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModuleCdc is a generic codec to be used throughout module
|
||||||
|
var ModuleCdc *codec.Codec
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cdc := codec.New()
|
||||||
|
RegisterCodec(cdc)
|
||||||
|
ModuleCdc = cdc
|
||||||
|
// ModuleCdc is not sealed so that other modules can register their own pubproposal and/or permission types.
|
||||||
|
|
||||||
|
// Register external module pubproposal types. Ideally these would be registered within the modules' types pkg init function.
|
||||||
|
// However registration happens here as a work-around.
|
||||||
|
RegisterProposalTypeCodec(distrtypes.CommunityPoolSpendProposal{}, "cosmos-sdk/CommunityPoolSpendProposal")
|
||||||
|
RegisterProposalTypeCodec(paramstypes.ParameterChangeProposal{}, "cosmos-sdk/ParameterChangeProposal")
|
||||||
|
RegisterProposalTypeCodec(govtypes.TextProposal{}, "cosmos-sdk/TextProposal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterCodec registers the necessary types for the module
|
||||||
|
func RegisterCodec(cdc *codec.Codec) {
|
||||||
|
|
||||||
|
// Proposals
|
||||||
|
cdc.RegisterInterface((*PubProposal)(nil), nil)
|
||||||
|
cdc.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil)
|
||||||
|
cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil)
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
cdc.RegisterInterface((*Permission)(nil), nil)
|
||||||
|
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)
|
||||||
|
cdc.RegisterConcrete(ParamChangePermission{}, "kava/ParamChangePermission", nil)
|
||||||
|
cdc.RegisterConcrete(TextPermission{}, "kava/TextPermission", nil)
|
||||||
|
|
||||||
|
// Msgs
|
||||||
|
cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil)
|
||||||
|
cdc.RegisterConcrete(MsgVote{}, "kava/MsgVote", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterPermissionTypeCodec allows external modules to register their own permission types on
|
||||||
|
// the internal ModuleCdc. This allows the MsgSubmitProposal to be correctly Amino encoded and
|
||||||
|
// decoded (when the msg contains a CommitteeChangeProposal).
|
||||||
|
func RegisterPermissionTypeCodec(o interface{}, name string) {
|
||||||
|
ModuleCdc.RegisterConcrete(o, name, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterProposalTypeCodec allows external modules to register their own pubproposal types on the
|
||||||
|
// internal ModuleCdc. This allows the MsgSubmitProposal to be correctly Amino encoded and decoded.
|
||||||
|
func RegisterProposalTypeCodec(o interface{}, name string) {
|
||||||
|
ModuleCdc.RegisterConcrete(o, name, nil)
|
||||||
|
}
|
140
x/committee/types/committee.go
Normal file
140
x/committee/types/committee.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MaxCommitteeDescriptionLength int = 512
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Committees
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions.
|
||||||
|
type Committee struct {
|
||||||
|
ID uint64 `json:"id" yaml:"id"`
|
||||||
|
Description string `json:"description" yaml:"description"`
|
||||||
|
Members []sdk.AccAddress `json:"members" yaml:"members"`
|
||||||
|
Permissions []Permission `json:"permissions" yaml:"permissions"`
|
||||||
|
VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` // Smallest percentage of members that must vote for a proposal to pass.
|
||||||
|
ProposalDuration time.Duration `json:"proposal_duration" yaml:"proposal_duration"` // The length of time a proposal remains active for. Proposals will close earlier if they get enough votes.
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission, threshold sdk.Dec, duration time.Duration) Committee {
|
||||||
|
return Committee{
|
||||||
|
ID: id,
|
||||||
|
Description: description,
|
||||||
|
Members: members,
|
||||||
|
Permissions: permissions,
|
||||||
|
VoteThreshold: threshold,
|
||||||
|
ProposalDuration: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Committee) HasMember(addr sdk.AccAddress) bool {
|
||||||
|
for _, m := range c.Members {
|
||||||
|
if m.Equals(addr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermissionsFor returns whether the committee is authorized to enact a proposal.
|
||||||
|
// As long as one permission allows the proposal then it goes through. Its the OR of all permissions.
|
||||||
|
func (c Committee) HasPermissionsFor(proposal PubProposal) bool {
|
||||||
|
for _, p := range c.Permissions {
|
||||||
|
if p.Allows(proposal) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Committee) Validate() error {
|
||||||
|
|
||||||
|
addressMap := make(map[string]bool, len(c.Members))
|
||||||
|
for _, m := range c.Members {
|
||||||
|
// check there are no duplicate members
|
||||||
|
if _, ok := addressMap[m.String()]; ok {
|
||||||
|
return fmt.Errorf("duplicate member found in committee, %s", m)
|
||||||
|
}
|
||||||
|
// check for valid addresses
|
||||||
|
if m.Empty() {
|
||||||
|
return fmt.Errorf("committee %d invalid: found empty member address", c.ID)
|
||||||
|
}
|
||||||
|
addressMap[m.String()] = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Members) == 0 {
|
||||||
|
return fmt.Errorf("committee %d invalid: cannot have zero members", c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Description) > MaxCommitteeDescriptionLength {
|
||||||
|
return fmt.Errorf("invalid description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// threshold must be in the range (0,1]
|
||||||
|
if c.VoteThreshold.IsNil() || c.VoteThreshold.LTE(sdk.ZeroDec()) || c.VoteThreshold.GT(sdk.NewDec(1)) {
|
||||||
|
return fmt.Errorf("invalid threshold")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ProposalDuration < 0 {
|
||||||
|
return fmt.Errorf("invalid proposal duration")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Proposals
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// PubProposal is the interface that all proposals must fulfill to be submitted to a committee.
|
||||||
|
// Proposal types can be created external to this module. For example a ParamChangeProposal, or CommunityPoolSpendProposal.
|
||||||
|
// It is pinned to the equivalent type in the gov module to create compatability between proposal types.
|
||||||
|
type PubProposal govtypes.Content
|
||||||
|
|
||||||
|
// Proposal is an internal record of a governance proposal submitted to a committee.
|
||||||
|
type Proposal struct {
|
||||||
|
PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
|
||||||
|
ID uint64 `json:"id" yaml:"id"`
|
||||||
|
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
|
||||||
|
Deadline time.Time `json:"deadline" yaml:"deadline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProposal(pubProposal PubProposal, id uint64, committeeID uint64, deadline time.Time) Proposal {
|
||||||
|
return Proposal{
|
||||||
|
PubProposal: pubProposal,
|
||||||
|
ID: id,
|
||||||
|
CommitteeID: committeeID,
|
||||||
|
Deadline: deadline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasExpiredBy calculates if the proposal will have expired by a certain time.
|
||||||
|
// All votes must be cast before deadline, those cast at time == deadline are not valid
|
||||||
|
func (p Proposal) HasExpiredBy(time time.Time) bool {
|
||||||
|
return !time.Before(p.Deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the fmt.Stringer interface, and importantly overrides the String methods inherited from the embedded PubProposal type.
|
||||||
|
func (p Proposal) String() string {
|
||||||
|
bz, _ := yaml.Marshal(p)
|
||||||
|
return string(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Votes
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
type Vote struct {
|
||||||
|
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
|
||||||
|
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
|
||||||
|
}
|
189
x/committee/types/committee_test.go
Normal file
189
x/committee/types/committee_test.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ PubProposal = UnregisteredPubProposal{}
|
||||||
|
|
||||||
|
type UnregisteredPubProposal struct {
|
||||||
|
govtypes.TextProposal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
|
||||||
|
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
|
||||||
|
|
||||||
|
type TypesTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
permissions []Permission
|
||||||
|
pubProposal PubProposal
|
||||||
|
expectHasPermissions bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal (single permission)",
|
||||||
|
permissions: []Permission{ParamChangePermission{
|
||||||
|
AllowedParams: AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectHasPermissions: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normal (multiple permissions)",
|
||||||
|
permissions: []Permission{
|
||||||
|
ParamChangePermission{
|
||||||
|
AllowedParams: AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
TextPermission{},
|
||||||
|
},
|
||||||
|
pubProposal: govtypes.NewTextProposal("A Proposal Title", "A description of this proposal"),
|
||||||
|
expectHasPermissions: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overruling permission",
|
||||||
|
permissions: []Permission{
|
||||||
|
ParamChangePermission{
|
||||||
|
AllowedParams: AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
GodPermission{},
|
||||||
|
},
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "CollateralParams",
|
||||||
|
|
||||||
|
Value: `[]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectHasPermissions: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no permissions",
|
||||||
|
permissions: nil,
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "CollateralParams",
|
||||||
|
|
||||||
|
Value: `[]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectHasPermissions: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "split permissions",
|
||||||
|
// These permissions looks like they allow the param change proposal, however a proposal must pass a single permission independently of others.
|
||||||
|
permissions: []Permission{
|
||||||
|
ParamChangePermission{
|
||||||
|
AllowedParams: AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
ParamChangePermission{
|
||||||
|
AllowedParams: AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtParams",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description of this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtParams",
|
||||||
|
|
||||||
|
Value: `[]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectHasPermissions: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unregistered proposal",
|
||||||
|
permissions: []Permission{
|
||||||
|
ParamChangePermission{
|
||||||
|
AllowedParams: AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
pubProposal: UnregisteredPubProposal{govtypes.TextProposal{"A Title", "A description."}},
|
||||||
|
expectHasPermissions: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
com := NewCommittee(
|
||||||
|
12,
|
||||||
|
"a description of this committee",
|
||||||
|
nil,
|
||||||
|
tc.permissions,
|
||||||
|
d("0.5"),
|
||||||
|
24*time.Hour,
|
||||||
|
)
|
||||||
|
suite.Equal(
|
||||||
|
tc.expectHasPermissions,
|
||||||
|
com.HasPermissionsFor(tc.pubProposal),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypesTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(TypesTestSuite))
|
||||||
|
}
|
16
x/committee/types/errors.go
Normal file
16
x/committee/types/errors.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownCommittee = sdkerrors.Register(ModuleName, 2, "committee not found")
|
||||||
|
ErrInvalidCommittee = sdkerrors.Register(ModuleName, 3, "invalid committee")
|
||||||
|
ErrUnknownProposal = sdkerrors.Register(ModuleName, 4, "proposal not found")
|
||||||
|
ErrProposalExpired = sdkerrors.Register(ModuleName, 5, "proposal expired")
|
||||||
|
ErrInvalidPubProposal = sdkerrors.Register(ModuleName, 6, "invalid pubproposal")
|
||||||
|
ErrUnknownVote = sdkerrors.Register(ModuleName, 7, "vote not found")
|
||||||
|
ErrInvalidGenesis = sdkerrors.Register(ModuleName, 8, "invalid genesis")
|
||||||
|
ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "pubproposal has no corresponding handler")
|
||||||
|
)
|
16
x/committee/types/events.go
Normal file
16
x/committee/types/events.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Module event types
|
||||||
|
const (
|
||||||
|
EventTypeProposalSubmit = "proposal_submit"
|
||||||
|
EventTypeProposalClose = "proposal_close"
|
||||||
|
EventTypeProposalVote = "proposal_vote"
|
||||||
|
|
||||||
|
AttributeValueCategory = "committee"
|
||||||
|
AttributeKeyCommitteeID = "committee_id"
|
||||||
|
AttributeKeyProposalID = "proposal_id"
|
||||||
|
AttributeKeyProposalCloseStatus = "status"
|
||||||
|
AttributeValueProposalPassed = "proposal_passed"
|
||||||
|
AttributeValueProposalTimeout = "proposal_timeout"
|
||||||
|
AttributeValueProposalFailed = "proposal_failed"
|
||||||
|
)
|
105
x/committee/types/genesis.go
Normal file
105
x/committee/types/genesis.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultNextProposalID is the starting poiint for proposal IDs.
|
||||||
|
const DefaultNextProposalID uint64 = 1
|
||||||
|
|
||||||
|
// GenesisState is state that must be provided at chain genesis.
|
||||||
|
type GenesisState struct {
|
||||||
|
NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"`
|
||||||
|
Committees []Committee `json:"committees" yaml:"committees"`
|
||||||
|
Proposals []Proposal `json:"proposals" yaml:"proposals"`
|
||||||
|
Votes []Vote `json:"votes" yaml:"votes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGenesisState returns a new genesis state object for the module.
|
||||||
|
func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState {
|
||||||
|
return GenesisState{
|
||||||
|
NextProposalID: nextProposalID,
|
||||||
|
Committees: committees,
|
||||||
|
Proposals: proposals,
|
||||||
|
Votes: votes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultGenesisState returns the default genesis state for the module.
|
||||||
|
func DefaultGenesisState() GenesisState {
|
||||||
|
return NewGenesisState(
|
||||||
|
DefaultNextProposalID,
|
||||||
|
[]Committee{},
|
||||||
|
[]Proposal{},
|
||||||
|
[]Vote{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal checks whether two gov GenesisState structs are equivalent
|
||||||
|
func (data GenesisState) Equal(data2 GenesisState) bool {
|
||||||
|
b1 := ModuleCdc.MustMarshalBinaryBare(data)
|
||||||
|
b2 := ModuleCdc.MustMarshalBinaryBare(data2)
|
||||||
|
return bytes.Equal(b1, b2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if a GenesisState is empty
|
||||||
|
func (data GenesisState) IsEmpty() bool {
|
||||||
|
return data.Equal(GenesisState{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs basic validation of genesis data.
|
||||||
|
func (gs GenesisState) Validate() error {
|
||||||
|
// validate committees
|
||||||
|
committeeMap := make(map[uint64]bool, len(gs.Committees))
|
||||||
|
for _, com := range gs.Committees {
|
||||||
|
// check there are no duplicate IDs
|
||||||
|
if _, ok := committeeMap[com.ID]; ok {
|
||||||
|
return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.ID)
|
||||||
|
}
|
||||||
|
committeeMap[com.ID] = true
|
||||||
|
|
||||||
|
// validate committee
|
||||||
|
if err := com.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate proposals - pp.Val, no duplicate IDs, no ids >= nextID, committee needs to exist
|
||||||
|
proposalMap := make(map[uint64]bool, len(gs.Proposals))
|
||||||
|
for _, p := range gs.Proposals {
|
||||||
|
// check there are no duplicate IDs
|
||||||
|
if _, ok := proposalMap[p.ID]; ok {
|
||||||
|
return fmt.Errorf("duplicate proposal ID found in genesis state; id: %d", p.ID)
|
||||||
|
}
|
||||||
|
proposalMap[p.ID] = true
|
||||||
|
|
||||||
|
// validate next proposal ID
|
||||||
|
if p.ID >= gs.NextProposalID {
|
||||||
|
return fmt.Errorf("NextProposalID is not greater than all proposal IDs; id: %d", p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check committee exists
|
||||||
|
if !committeeMap[p.CommitteeID] {
|
||||||
|
return fmt.Errorf("proposal refers to non existant committee; proposal: %+v", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate pubProposal
|
||||||
|
if err := p.PubProposal.ValidateBasic(); err != nil {
|
||||||
|
return fmt.Errorf("proposal %d invalid: %w", p.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate votes
|
||||||
|
for _, v := range gs.Votes {
|
||||||
|
// check proposal exists
|
||||||
|
if !proposalMap[v.ProposalID] {
|
||||||
|
return fmt.Errorf("vote refers to non existant proposal; vote: %+v", v)
|
||||||
|
}
|
||||||
|
// validate address
|
||||||
|
if v.Voter.Empty() {
|
||||||
|
return fmt.Errorf("found empty voter address; vote: %+v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
169
x/committee/types/genesis_test.go
Normal file
169
x/committee/types/genesis_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
"github.com/tendermint/tendermint/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) }
|
||||||
|
func TestGenesisState_Validate(t *testing.T) {
|
||||||
|
testTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
addresses := []sdk.AccAddress{
|
||||||
|
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
|
||||||
|
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
|
||||||
|
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest3"))),
|
||||||
|
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest4"))),
|
||||||
|
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest5"))),
|
||||||
|
}
|
||||||
|
testGenesis := GenesisState{
|
||||||
|
NextProposalID: 2,
|
||||||
|
Committees: []Committee{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Description: "This committee is for testing.",
|
||||||
|
Members: addresses[:3],
|
||||||
|
Permissions: []Permission{GodPermission{}},
|
||||||
|
VoteThreshold: d("0.667"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Description: "This committee is also for testing.",
|
||||||
|
Members: addresses[2:],
|
||||||
|
Permissions: nil,
|
||||||
|
VoteThreshold: d("0.8"),
|
||||||
|
ProposalDuration: time.Hour * 24 * 21,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Proposals: []Proposal{
|
||||||
|
{ID: 1, CommitteeID: 1, PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
|
||||||
|
},
|
||||||
|
Votes: []Vote{
|
||||||
|
{ProposalID: 1, Voter: addresses[0]},
|
||||||
|
{ProposalID: 1, Voter: addresses[1]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
genState GenesisState
|
||||||
|
expectPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
genState: DefaultGenesisState(),
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
genState: testGenesis,
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate committee IDs",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID,
|
||||||
|
Committees: append(testGenesis.Committees, testGenesis.Committees[0]),
|
||||||
|
Proposals: testGenesis.Proposals,
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid committee",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID,
|
||||||
|
Committees: append(testGenesis.Committees, Committee{}),
|
||||||
|
Proposals: testGenesis.Proposals,
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate proposal IDs",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID,
|
||||||
|
Committees: testGenesis.Committees,
|
||||||
|
Proposals: append(testGenesis.Proposals, testGenesis.Proposals[0]),
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid NextProposalID",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: 0,
|
||||||
|
Committees: testGenesis.Committees,
|
||||||
|
Proposals: testGenesis.Proposals,
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proposal without committee",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID + 1,
|
||||||
|
Committees: testGenesis.Committees,
|
||||||
|
Proposals: append(
|
||||||
|
testGenesis.Proposals,
|
||||||
|
Proposal{
|
||||||
|
ID: testGenesis.NextProposalID,
|
||||||
|
PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
CommitteeID: 247, // doesn't exist
|
||||||
|
}),
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid proposal",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID,
|
||||||
|
Committees: testGenesis.Committees,
|
||||||
|
Proposals: append(testGenesis.Proposals, Proposal{}),
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vote without proposal",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID,
|
||||||
|
Committees: testGenesis.Committees,
|
||||||
|
Proposals: nil,
|
||||||
|
Votes: testGenesis.Votes,
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid vote",
|
||||||
|
genState: GenesisState{
|
||||||
|
NextProposalID: testGenesis.NextProposalID,
|
||||||
|
Committees: testGenesis.Committees,
|
||||||
|
Proposals: testGenesis.Proposals,
|
||||||
|
Votes: append(testGenesis.Votes, Vote{}),
|
||||||
|
},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
err := tc.genState.Validate()
|
||||||
|
|
||||||
|
if tc.expectPass {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
x/committee/types/keys.go
Normal file
54
x/committee/types/keys.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ModuleName The name that will be used throughout the module
|
||||||
|
ModuleName = "committee"
|
||||||
|
|
||||||
|
// StoreKey Top level store key where all module items will be stored
|
||||||
|
StoreKey = ModuleName
|
||||||
|
|
||||||
|
// RouterKey Top level router key
|
||||||
|
RouterKey = ModuleName
|
||||||
|
|
||||||
|
// QuerierRoute Top level query string
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
|
||||||
|
// DefaultParamspace default name for parameter store
|
||||||
|
DefaultParamspace = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key prefixes
|
||||||
|
var (
|
||||||
|
CommitteeKeyPrefix = []byte{0x00} // prefix for keys that store committees
|
||||||
|
ProposalKeyPrefix = []byte{0x01} // prefix for keys that store proposals
|
||||||
|
VoteKeyPrefix = []byte{0x02} // prefix for keys that store votes
|
||||||
|
|
||||||
|
NextProposalIDKey = []byte{0x03} // key for the next proposal id
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetKeyFromID returns the bytes to use as a key for a uint64 id
|
||||||
|
func GetKeyFromID(id uint64) []byte {
|
||||||
|
return uint64ToBytes(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVoteKey(proposalID uint64, voter sdk.AccAddress) []byte {
|
||||||
|
return append(GetKeyFromID(proposalID), voter.Bytes()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys.
|
||||||
|
func uint64ToBytes(id uint64) []byte {
|
||||||
|
bz := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(bz, uint64(id))
|
||||||
|
return bz
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64FromBytes converts some fixed length bytes back into a uint64.
|
||||||
|
func Uint64FromBytes(bz []byte) uint64 {
|
||||||
|
return binary.BigEndian.Uint64(bz)
|
||||||
|
}
|
94
x/committee/types/msg.go
Normal file
94
x/committee/types/msg.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeMsgSubmitProposal = "commmittee_submit_proposal" // 'committee' prefix appended to avoid potential conflicts with gov msg types
|
||||||
|
TypeMsgVote = "committee_vote"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _, _ sdk.Msg = MsgSubmitProposal{}, MsgVote{}
|
||||||
|
|
||||||
|
// MsgSubmitProposal is used by committee members to create a new proposal that they can vote on.
|
||||||
|
type MsgSubmitProposal struct {
|
||||||
|
PubProposal PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
|
||||||
|
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
|
||||||
|
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgSubmitProposal creates a new MsgSubmitProposal instance
|
||||||
|
func NewMsgSubmitProposal(pubProposal PubProposal, proposer sdk.AccAddress, committeeID uint64) MsgSubmitProposal {
|
||||||
|
return MsgSubmitProposal{
|
||||||
|
PubProposal: pubProposal,
|
||||||
|
Proposer: proposer,
|
||||||
|
CommitteeID: committeeID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgSubmitProposal) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within events.
|
||||||
|
func (msg MsgSubmitProposal) Type() string { return TypeMsgSubmitProposal }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||||
|
func (msg MsgSubmitProposal) ValidateBasic() error {
|
||||||
|
if msg.PubProposal == nil {
|
||||||
|
return sdkerrors.Wrap(ErrInvalidPubProposal, "pub proposal cannot be nil")
|
||||||
|
}
|
||||||
|
if msg.Proposer.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "proposer address cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg.PubProposal.ValidateBasic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||||
|
func (msg MsgSubmitProposal) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign.
|
||||||
|
func (msg MsgSubmitProposal) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Proposer}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgVote is submitted by committee members to vote on proposals.
|
||||||
|
type MsgVote struct {
|
||||||
|
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
|
||||||
|
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgVote creates a message to cast a vote on an active proposal
|
||||||
|
func NewMsgVote(voter sdk.AccAddress, proposalID uint64) MsgVote {
|
||||||
|
return MsgVote{proposalID, voter}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgVote) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within events.
|
||||||
|
func (msg MsgVote) Type() string { return TypeMsgVote }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||||
|
func (msg MsgVote) ValidateBasic() error {
|
||||||
|
if msg.Voter.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "voter address cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||||
|
func (msg MsgVote) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign.
|
||||||
|
func (msg MsgVote) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Voter}
|
||||||
|
}
|
81
x/committee/types/msg_test.go
Normal file
81
x/committee/types/msg_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMsgSubmitProposal_ValidateBasic(t *testing.T) {
|
||||||
|
addr := sdk.AccAddress([]byte("someName"))
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msg MsgSubmitProposal
|
||||||
|
expectPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
msg: MsgSubmitProposal{govtypes.NewTextProposal("A Title", "A proposal description."), addr, 3},
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty address",
|
||||||
|
msg: MsgSubmitProposal{govtypes.NewTextProposal("A Title", "A proposal description."), nil, 3},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid proposal",
|
||||||
|
msg: MsgSubmitProposal{govtypes.TextProposal{}, addr, 3},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
err := tc.msg.ValidateBasic()
|
||||||
|
|
||||||
|
if tc.expectPass {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgVote_ValidateBasic(t *testing.T) {
|
||||||
|
addr := sdk.AccAddress([]byte("someName"))
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msg MsgVote
|
||||||
|
expectPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
msg: MsgVote{5, addr},
|
||||||
|
expectPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty address",
|
||||||
|
msg: MsgVote{5, nil},
|
||||||
|
expectPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
err := tc.msg.ValidateBasic()
|
||||||
|
|
||||||
|
if tc.expectPass {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
113
x/committee/types/permissions.go
Normal file
113
x/committee/types/permissions.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go).
|
||||||
|
// But since these proposals contain Permissions, these types also need registering:
|
||||||
|
govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil)
|
||||||
|
govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission")
|
||||||
|
govtypes.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission")
|
||||||
|
govtypes.RegisterProposalTypeCodec(TextPermission{}, "kava/TextPermission")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission is anything with a method that validates whether a proposal is allowed by it or not.
|
||||||
|
type Permission interface {
|
||||||
|
Allows(PubProposal) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// GodPermission
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// GodPermission allows any governance proposal. It is used mainly for testing.
|
||||||
|
type GodPermission struct{}
|
||||||
|
|
||||||
|
var _ Permission = GodPermission{}
|
||||||
|
|
||||||
|
func (GodPermission) Allows(PubProposal) bool { return true }
|
||||||
|
|
||||||
|
func (GodPermission) MarshalYAML() (interface{}, error) {
|
||||||
|
valueToMarshal := struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
}{
|
||||||
|
Type: "god_permission",
|
||||||
|
}
|
||||||
|
return valueToMarshal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// ParamChangePermission
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// ParamChangeProposal only allows changes to certain params
|
||||||
|
type ParamChangePermission struct {
|
||||||
|
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Permission = ParamChangePermission{}
|
||||||
|
|
||||||
|
func (perm ParamChangePermission) Allows(p PubProposal) bool {
|
||||||
|
proposal, ok := p.(paramstypes.ParameterChangeProposal)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, change := range proposal.Changes {
|
||||||
|
if !perm.AllowedParams.Contains(change) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (perm ParamChangePermission) MarshalYAML() (interface{}, error) {
|
||||||
|
valueToMarshal := struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
AllowedParams AllowedParams `yaml:"allowed_params"`
|
||||||
|
}{
|
||||||
|
Type: "param_change_permission",
|
||||||
|
AllowedParams: perm.AllowedParams,
|
||||||
|
}
|
||||||
|
return valueToMarshal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllowedParam struct {
|
||||||
|
Subspace string `json:"subspace" yaml:"subspace"`
|
||||||
|
Key string `json:"key" yaml:"key"`
|
||||||
|
}
|
||||||
|
type AllowedParams []AllowedParam
|
||||||
|
|
||||||
|
func (allowed AllowedParams) Contains(paramChange paramstypes.ParamChange) bool {
|
||||||
|
for _, p := range allowed {
|
||||||
|
if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// TextPermission
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
// TextPermission allows any text governance proposal.
|
||||||
|
type TextPermission struct{}
|
||||||
|
|
||||||
|
var _ Permission = TextPermission{}
|
||||||
|
|
||||||
|
func (TextPermission) Allows(p PubProposal) bool {
|
||||||
|
_, ok := p.(govtypes.TextProposal)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TextPermission) MarshalYAML() (interface{}, error) {
|
||||||
|
valueToMarshal := struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
}{
|
||||||
|
Type: "text_permission",
|
||||||
|
}
|
||||||
|
return valueToMarshal, nil
|
||||||
|
}
|
286
x/committee/types/permissions_test.go
Normal file
286
x/committee/types/permissions_test.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PermissionsTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
exampleAllowedParams AllowedParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PermissionsTestSuite) SetupTest() {
|
||||||
|
suite.exampleAllowedParams = AllowedParams{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "SurplusThreshold",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "CollateralParams",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subspace: "auction",
|
||||||
|
Key: "BidDuration",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
allowedParams AllowedParams
|
||||||
|
pubProposal PubProposal
|
||||||
|
expectAllowed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal (single param)",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description for this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normal (multiple params)",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description for this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "CollateralParams",
|
||||||
|
|
||||||
|
Value: `[]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not allowed (not in list)",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description for this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "GlobalDebtLimit",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000000"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectAllowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not allowed (nil allowed params)",
|
||||||
|
allowedParams: nil,
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description for this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `[{"denom": "usdx", "amount": "1000000"}]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectAllowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not allowed (mismatched pubproposal type)",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
|
||||||
|
expectAllowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not allowed (nil pubproposal)",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
pubProposal: nil,
|
||||||
|
expectAllowed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
permission := ParamChangePermission{
|
||||||
|
AllowedParams: tc.allowedParams,
|
||||||
|
}
|
||||||
|
suite.Equal(
|
||||||
|
tc.expectAllowed,
|
||||||
|
permission.Allows(tc.pubProposal),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PermissionsTestSuite) TestAllowedParams_Contains() {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
allowedParams AllowedParams
|
||||||
|
testParam paramstypes.ParamChange
|
||||||
|
expectContained bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
testParam: paramstypes.ParamChange{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
expectContained: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing subspace",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
testParam: paramstypes.ParamChange{
|
||||||
|
Subspace: "",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
expectContained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing key",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
testParam: paramstypes.ParamChange{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
expectContained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty list",
|
||||||
|
allowedParams: AllowedParams{},
|
||||||
|
testParam: paramstypes.ParamChange{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
expectContained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil list",
|
||||||
|
allowedParams: nil,
|
||||||
|
testParam: paramstypes.ParamChange{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
expectContained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no param change",
|
||||||
|
allowedParams: suite.exampleAllowedParams,
|
||||||
|
testParam: paramstypes.ParamChange{},
|
||||||
|
expectContained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty list and no param change",
|
||||||
|
allowedParams: AllowedParams{},
|
||||||
|
testParam: paramstypes.ParamChange{},
|
||||||
|
expectContained: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
suite.Require().Equal(
|
||||||
|
tc.expectContained,
|
||||||
|
tc.allowedParams.Contains(tc.testParam),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PermissionsTestSuite) TestTextPermission_Allows() {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
pubProposal PubProposal
|
||||||
|
expectAllowed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
pubProposal: govtypes.NewTextProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description for this proposal.",
|
||||||
|
),
|
||||||
|
expectAllowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not allowed (wrong pubproposal type)",
|
||||||
|
pubProposal: paramstypes.NewParameterChangeProposal(
|
||||||
|
"A Title",
|
||||||
|
"A description for this proposal.",
|
||||||
|
[]paramstypes.ParamChange{
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "DebtThreshold",
|
||||||
|
Value: `{"denom": "usdx", "amount": "1000000"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Subspace: "cdp",
|
||||||
|
Key: "CollateralParams",
|
||||||
|
Value: `[]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
expectAllowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not allowed (nil pubproposal)",
|
||||||
|
pubProposal: nil,
|
||||||
|
expectAllowed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
permission := TextPermission{}
|
||||||
|
suite.Equal(
|
||||||
|
tc.expectAllowed,
|
||||||
|
permission.Allows(tc.pubProposal),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPermissionsTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(PermissionsTestSuite))
|
||||||
|
}
|
108
x/committee/types/proposal.go
Normal file
108
x/committee/types/proposal.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProposalTypeCommitteeChange = "CommitteeChange"
|
||||||
|
ProposalTypeCommitteeDelete = "CommitteeDelete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ensure proposal types fulfill the PubProposal interface and the gov Content interface.
|
||||||
|
var _, _ govtypes.Content = CommitteeChangeProposal{}, CommitteeDeleteProposal{}
|
||||||
|
var _, _ PubProposal = CommitteeChangeProposal{}, CommitteeDeleteProposal{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded.
|
||||||
|
govtypes.RegisterProposalType(ProposalTypeCommitteeChange)
|
||||||
|
govtypes.RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal")
|
||||||
|
|
||||||
|
govtypes.RegisterProposalType(ProposalTypeCommitteeDelete)
|
||||||
|
govtypes.RegisterProposalTypeCodec(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one.
|
||||||
|
type CommitteeChangeProposal struct {
|
||||||
|
Title string `json:"title" yaml:"title"`
|
||||||
|
Description string `json:"description" yaml:"description"`
|
||||||
|
NewCommittee Committee `json:"new_committee" yaml:"new_committee"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommitteeChangeProposal(title string, description string, newCommittee Committee) CommitteeChangeProposal {
|
||||||
|
return CommitteeChangeProposal{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
NewCommittee: newCommittee,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTitle returns the title of the proposal.
|
||||||
|
func (ccp CommitteeChangeProposal) GetTitle() string { return ccp.Title }
|
||||||
|
|
||||||
|
// GetDescription returns the description of the proposal.
|
||||||
|
func (ccp CommitteeChangeProposal) GetDescription() string { return ccp.Description }
|
||||||
|
|
||||||
|
// ProposalRoute returns the routing key of the proposal.
|
||||||
|
func (ccp CommitteeChangeProposal) ProposalRoute() string { return RouterKey }
|
||||||
|
|
||||||
|
// ProposalType returns the type of the proposal.
|
||||||
|
func (ccp CommitteeChangeProposal) ProposalType() string { return ProposalTypeCommitteeChange }
|
||||||
|
|
||||||
|
// ValidateBasic runs basic stateless validity checks
|
||||||
|
func (ccp CommitteeChangeProposal) ValidateBasic() error {
|
||||||
|
if err := govtypes.ValidateAbstract(ccp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ccp.NewCommittee.Validate(); err != nil {
|
||||||
|
return sdkerrors.Wrap(ErrInvalidCommittee, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (ccp CommitteeChangeProposal) String() string {
|
||||||
|
bz, _ := yaml.Marshal(ccp)
|
||||||
|
return string(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitteeDeleteProposal is a gov proposal for removing a committee.
|
||||||
|
type CommitteeDeleteProposal struct {
|
||||||
|
Title string `json:"title" yaml:"title"`
|
||||||
|
Description string `json:"description" yaml:"description"`
|
||||||
|
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommitteeDeleteProposal(title string, description string, committeeID uint64) CommitteeDeleteProposal {
|
||||||
|
return CommitteeDeleteProposal{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
CommitteeID: committeeID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTitle returns the title of the proposal.
|
||||||
|
func (cdp CommitteeDeleteProposal) GetTitle() string { return cdp.Title }
|
||||||
|
|
||||||
|
// GetDescription returns the description of the proposal.
|
||||||
|
func (cdp CommitteeDeleteProposal) GetDescription() string { return cdp.Description }
|
||||||
|
|
||||||
|
// ProposalRoute returns the routing key of the proposal.
|
||||||
|
func (cdp CommitteeDeleteProposal) ProposalRoute() string { return RouterKey }
|
||||||
|
|
||||||
|
// ProposalType returns the type of the proposal.
|
||||||
|
func (cdp CommitteeDeleteProposal) ProposalType() string { return ProposalTypeCommitteeDelete }
|
||||||
|
|
||||||
|
// ValidateBasic runs basic stateless validity checks
|
||||||
|
func (cdp CommitteeDeleteProposal) ValidateBasic() error {
|
||||||
|
return govtypes.ValidateAbstract(cdp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (cdp CommitteeDeleteProposal) String() string {
|
||||||
|
bz, _ := yaml.Marshal(cdp)
|
||||||
|
return string(bz)
|
||||||
|
}
|
48
x/committee/types/querier.go
Normal file
48
x/committee/types/querier.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Query endpoints supported by the Querier
|
||||||
|
const (
|
||||||
|
QueryCommittees = "committees"
|
||||||
|
QueryCommittee = "committee"
|
||||||
|
QueryProposals = "proposals"
|
||||||
|
QueryProposal = "proposal"
|
||||||
|
QueryVotes = "votes"
|
||||||
|
QueryVote = "vote"
|
||||||
|
QueryTally = "tally"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueryCommitteeParams struct {
|
||||||
|
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueryCommitteeParams(committeeID uint64) QueryCommitteeParams {
|
||||||
|
return QueryCommitteeParams{
|
||||||
|
CommitteeID: committeeID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryProposalParams struct {
|
||||||
|
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueryProposalParams(proposalID uint64) QueryProposalParams {
|
||||||
|
return QueryProposalParams{
|
||||||
|
ProposalID: proposalID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryVoteParams struct {
|
||||||
|
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
|
||||||
|
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueryVoteParams(proposalID uint64, voter sdk.AccAddress) QueryVoteParams {
|
||||||
|
return QueryVoteParams{
|
||||||
|
ProposalID: proposalID,
|
||||||
|
Voter: voter,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user