diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b996d02..0ab90f81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,7 +45,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
[\#584](https://github.com/Kava-Labs/kava/pulls/584) Add REST client and CLI queries for `kavadist` module
[\#578](https://github.com/Kava-Labs/kava/pulls/578) Add v0.3 compatible REST client that supports
-```
+
+```plaintext
/v0_3/node_info
/v0_3/auth/accounts/
/v0_3/
@@ -55,6 +56,8 @@ Ref: https://keepachangelog.com/en/1.0.0/
/v0_3/distribution/delegators//rewards
```
+[\#598](https://github.com/Kava-Labs/kava/pulls/598) CLI and REST queries for committee proposals (ie `kvcli q committee proposal 1`) now query the historical state to return the proposal object before it was deleted from state
+
## [v0.8.1](https://github.com/Kava-Labs/kava/releases/tag/v0.8.1) kava-3 Patch Release
This version mitigates a memory leak in tendermint that was found prior to launching kava-3. It is fully compatible with v0.8.0 and is intended to replace that version as the canonical software version for upgrading the Kava mainnet from kava-2 to kava-3. Note that there are no breaking changes between the versions, but a safety check was added to this version to prevent starting the node with an unsafe configuration.
diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go
index 8bda3128..c44170fe 100644
--- a/x/committee/client/cli/query.go
+++ b/x/committee/client/cli/query.go
@@ -130,20 +130,12 @@ func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command {
if err != nil {
return fmt.Errorf("proposal-id %s not a valid uint", args[0])
}
- bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
+
+ proposal, _, err := common.QueryProposalByID(cliCtx, cdc, queryRoute, 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)
},
}
diff --git a/x/committee/client/common/query.go b/x/committee/client/common/query.go
new file mode 100644
index 00000000..89de2353
--- /dev/null
+++ b/x/committee/client/common/query.go
@@ -0,0 +1,164 @@
+package common
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/cosmos/cosmos-sdk/client/context"
+ "github.com/cosmos/cosmos-sdk/codec"
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
+ "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)
+}
+
+// QueryProposalByID returns a proposal from state if present or fallbacks to searching old blocks
+func QueryProposalByID(cliCtx context.CLIContext, cdc *codec.Codec, queryRoute string, proposalID uint64) (*types.Proposal, int64, error) {
+ bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
+ if err != nil {
+ return nil, 0, err
+ }
+
+ res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz)
+
+ if err == nil {
+ var proposal *types.Proposal
+ cdc.MustUnmarshalJSON(res, &proposal)
+
+ return proposal, height, nil
+ }
+
+ // NOTE: !errors.Is(err, types.ErrUnknownProposal) does not work here
+ if err != nil && !strings.Contains(err.Error(), "proposal not found") {
+ return nil, 0, err
+ }
+
+ res, height, err = cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryNextProposalID), nil)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ var nextProposalID uint64
+ cdc.MustUnmarshalJSON(res, &nextProposalID)
+
+ if proposalID >= nextProposalID {
+ return nil, 0, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
+ }
+
+ 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))),
+ }
+
+ searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ for _, info := range searchResult.Txs {
+ for _, msg := range info.Tx.GetMsgs() {
+ if msg.Type() == types.TypeMsgSubmitProposal {
+ subMsg := msg.(types.MsgSubmitProposal)
+
+ deadline, err := calculateDeadline(cliCtx, cdc, queryRoute, subMsg.CommitteeID, info.Height)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return &types.Proposal{
+ ID: proposalID,
+ CommitteeID: subMsg.CommitteeID,
+ PubProposal: subMsg.PubProposal,
+ Deadline: deadline,
+ }, height, nil
+ }
+ }
+ }
+
+ return nil, 0, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
+}
+
+// calculateDeadline returns the proposal deadline for a committee and block height
+func calculateDeadline(cliCtx context.CLIContext, cdc *codec.Codec, queryRoute string, committeeID uint64, blockHeight int64) (time.Time, error) {
+ var deadline time.Time
+
+ bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
+ if err != nil {
+ return deadline, err
+ }
+
+ res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittee), bz)
+ if err != nil {
+ return deadline, err
+ }
+
+ var committee types.Committee
+ err = cdc.UnmarshalJSON(res, &committee)
+ if err != nil {
+ return deadline, err
+ }
+
+ node, err := cliCtx.GetNode()
+ if err != nil {
+ return deadline, err
+ }
+
+ resultBlock, err := node.Block(&blockHeight)
+ if err != nil {
+ return deadline, err
+ }
+
+ deadline = resultBlock.Block.Header.Time.Add(committee.ProposalDuration)
+ return deadline, nil
+}
diff --git a/x/committee/client/common/query_proposer.go b/x/committee/client/common/query_proposer.go
deleted file mode 100644
index d6f12a56..00000000
--- a/x/committee/client/common/query_proposer.go
+++ /dev/null
@@ -1,60 +0,0 @@
-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)
-}
diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go
index 5e6b4590..cf182065 100644
--- a/x/committee/client/rest/query.go
+++ b/x/committee/client/rest/query.go
@@ -148,14 +148,14 @@ func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
if !ok {
return
}
- bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
+
+ proposal, height, err := common.QueryProposalByID(cliCtx, cliCtx.Codec, types.ModuleName, proposalID)
if err != nil {
- rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
+ rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
- // Query
- res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposal), bz)
+ res, err := cliCtx.Codec.MarshalJSON(proposal)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
diff --git a/x/committee/keeper/querier.go b/x/committee/keeper/querier.go
index a098854b..d48bcd6b 100644
--- a/x/committee/keeper/querier.go
+++ b/x/committee/keeper/querier.go
@@ -4,7 +4,6 @@ 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"
@@ -29,6 +28,8 @@ func NewQuerier(keeper Keeper) sdk.Querier {
return queryVote(ctx, path[1:], req, keeper)
case types.QueryTally:
return queryTally(ctx, path[1:], req, keeper)
+ case types.QueryNextProposalID:
+ return queryNextProposalID(ctx, req, keeper)
case types.QueryRawParams:
return queryRawParams(ctx, path[1:], req, keeper)
@@ -111,6 +112,16 @@ func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper
return bz, nil
}
+func queryNextProposalID(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
+ nextProposalID, _ := keeper.GetNextProposalID(ctx)
+
+ bz, err := types.ModuleCdc.MarshalJSON(nextProposalID)
+ if err != nil {
+ return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
+ }
+ return bz, nil
+}
+
// ------------------------------------------
// Votes
// ------------------------------------------
diff --git a/x/committee/keeper/querier_test.go b/x/committee/keeper/querier_test.go
index 9c13fda1..3c901e91 100644
--- a/x/committee/keeper/querier_test.go
+++ b/x/committee/keeper/querier_test.go
@@ -174,6 +174,18 @@ func (suite *QuerierTestSuite) TestQueryProposal() {
suite.Equal(suite.testGenesis.Proposals[0], proposal)
}
+func (suite *QuerierTestSuite) TestQueryNextProposalID() {
+ bz, err := suite.querier(suite.ctx, []string{types.QueryNextProposalID}, abci.RequestQuery{})
+ suite.Require().NoError(err)
+ suite.Require().NotNil(bz)
+
+ var nextProposalID uint64
+ suite.Require().NoError(suite.cdc.UnmarshalJSON(bz, &nextProposalID))
+
+ expectedID, _ := suite.keeper.GetNextProposalID(suite.ctx)
+ suite.Require().Equal(expectedID, nextProposalID)
+}
+
func (suite *QuerierTestSuite) TestQueryVotes() {
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
diff --git a/x/committee/types/querier.go b/x/committee/types/querier.go
index 5512323f..29970036 100644
--- a/x/committee/types/querier.go
+++ b/x/committee/types/querier.go
@@ -6,14 +6,15 @@ import (
// Query endpoints supported by the Querier
const (
- QueryCommittees = "committees"
- QueryCommittee = "committee"
- QueryProposals = "proposals"
- QueryProposal = "proposal"
- QueryVotes = "votes"
- QueryVote = "vote"
- QueryTally = "tally"
- QueryRawParams = "raw_params"
+ QueryCommittees = "committees"
+ QueryCommittee = "committee"
+ QueryProposals = "proposals"
+ QueryProposal = "proposal"
+ QueryNextProposalID = "next-proposal-id"
+ QueryVotes = "votes"
+ QueryVote = "vote"
+ QueryTally = "tally"
+ QueryRawParams = "raw_params"
)
type QueryCommitteeParams struct {