From b2edeb854971a4f31e05d39d4544124aebdb9ab5 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Sun, 24 May 2020 19:27:11 -0700 Subject: [PATCH] [R4R] Add flags to auction queries (#522) * cli auction query flags * update bep3 filter method name * rest auction query flags * add constants for auction type, phase * fix test * revisions --- x/auction/client/cli/query.go | 102 ++++++++++++++++++++++++++++--- x/auction/client/rest/query.go | 58 ++++++++++++++++-- x/auction/client/rest/rest.go | 16 +++++ x/auction/client/rest/tx.go | 6 -- x/auction/keeper/keeper.go | 9 +++ x/auction/keeper/querier.go | 59 +++++++++++++++--- x/auction/keeper/querier_test.go | 4 +- x/auction/types/auctions.go | 22 ++++--- x/auction/types/querier.go | 12 +++- x/bep3/keeper/querier.go | 2 +- 10 files changed, 250 insertions(+), 40 deletions(-) diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go index 59ee6620..37910226 100644 --- a/x/auction/client/cli/query.go +++ b/x/auction/client/cli/query.go @@ -3,16 +3,26 @@ package cli import ( "fmt" "strconv" + "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" "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/kava-labs/kava/x/auction/types" ) +// Query auction flags +const ( + flagType = "type" + flagDenom = "denom" + flagPhase = "phase" +) + // GetQueryCmd returns the cli query commands for this module func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Group nameservice queries under a subcommand @@ -52,7 +62,7 @@ func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Query - res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuction), bz) + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuction), bz) if err != nil { return err } @@ -61,6 +71,8 @@ func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { var auction types.Auction cdc.MustUnmarshalJSON(res, &auction) auctionWithPhase := types.NewAuctionWithPhase(auction) + + cliCtx = cliCtx.WithHeight(height) return cliCtx.PrintOutput(auctionWithPhase) }, } @@ -68,30 +80,99 @@ func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // QueryGetAuctionsCmd queries the auctions in the store func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "auctions", - Short: "get a list of active auctions", - Args: cobra.NoArgs, + Short: "query auctions with optional filters", + Long: strings.TrimSpace(`Query for all paginated auctions that match optional filters: +Example: +$ kvcli q auction auctions --type=(collateral|surplus|debt) +$ kvcli q auction auctions --denom=bnb +$ kvcli q auction auctions --phase=(forward|reverse) +$ kvcli q auction auctions --page=2 --limit=100 +`, + ), RunE: func(cmd *cobra.Command, args []string) error { + strType := viper.GetString(flagType) + strDenom := viper.GetString(flagDenom) + strPhase := viper.GetString(flagPhase) + page := viper.GetInt(flags.FlagPage) + limit := viper.GetInt(flags.FlagLimit) + + var ( + auctionType string + auctionDenom string + auctionPhase string + ) + + params := types.NewQueryAllAuctionParams(page, limit, auctionType, auctionDenom, auctionPhase) + + if len(strType) != 0 { + auctionType = strings.ToLower(strings.TrimSpace(strType)) + if auctionType != types.CollateralAuctionType && + auctionType != types.SurplusAuctionType && + auctionType != types.DebtAuctionType { + return fmt.Errorf("invalid auction type %s", strType) + } + params.Type = auctionType + } + + if len(strDenom) != 0 { + auctionDenom := strings.TrimSpace(strDenom) + err := sdk.ValidateDenom(auctionDenom) + if err != nil { + return err + } + params.Denom = auctionDenom + } + + if len(strPhase) != 0 { + auctionPhase := strings.ToLower(strings.TrimSpace(strPhase)) + if auctionType != types.CollateralAuctionType && len(auctionType) > 0 { + return fmt.Errorf("cannot apply phase flag to non-collateral auction type") + } + if auctionPhase != types.ForwardAuctionPhase && auctionPhase != types.ReverseAuctionPhase { + return fmt.Errorf("invalid auction phase %s", strPhase) + } + params.Phase = auctionPhase + } + + bz, err := cdc.MarshalJSON(params) + if err != nil { + return err + } + cliCtx := context.NewCLIContext().WithCodec(cdc) // Query - res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuctions), nil) + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuctions), bz) if err != nil { return err } // Decode and print results - var auctions types.Auctions - cdc.MustUnmarshalJSON(res, &auctions) + var matchingAuctions types.Auctions + cdc.MustUnmarshalJSON(res, &matchingAuctions) + + if len(matchingAuctions) == 0 { + return fmt.Errorf("No matching auctions found") + } auctionsWithPhase := []types.AuctionWithPhase{} // using empty slice so json returns [] instead of null when there's no auctions - for _, a := range auctions { + for _, a := range matchingAuctions { auctionsWithPhase = append(auctionsWithPhase, types.NewAuctionWithPhase(a)) } - return cliCtx.PrintOutput(auctionsWithPhase) + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(auctionsWithPhase) // nolint:errcheck }, } + + cmd.Flags().Int(flags.FlagPage, 1, "pagination page of auctions to to query for") + cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit of auctions to query for") + cmd.Flags().String(flagType, "", "(optional) filter by auction type, type: collateral, debt, surplus") + cmd.Flags().String(flagDenom, "", "(optional) filter by auction denom") + cmd.Flags().String(flagPhase, "", "(optional) filter by collateral auction phase, phase: forward/reverse") + + return cmd } // QueryParamsCmd queries the auction module parameters @@ -106,7 +187,7 @@ func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Query route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams) - res, _, err := cliCtx.QueryWithData(route, nil) + res, height, err := cliCtx.QueryWithData(route, nil) if err != nil { return err } @@ -114,6 +195,7 @@ func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Decode and print results var out types.Params cdc.MustUnmarshalJSON(res, &out) + cliCtx = cliCtx.WithHeight(height) return cliCtx.PrintOutput(out) }, } diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go index 374c2246..f14c3e29 100644 --- a/x/auction/client/rest/query.go +++ b/x/auction/client/rest/query.go @@ -3,10 +3,12 @@ package rest import ( "fmt" "net/http" + "strings" "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/kava-labs/kava/x/auction/types" @@ -68,22 +70,70 @@ func queryAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + _, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + // Parse the query height cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) if !ok { return } - // Get all auctions - res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetAuctions), nil) + var auctionType string + var auctionDenom string + var auctionPhase string + + if x := r.URL.Query().Get(RestType); len(x) != 0 { + auctionType = strings.ToLower(strings.TrimSpace(x)) + if auctionType != types.CollateralAuctionType && + auctionType != types.SurplusAuctionType && + auctionType != types.DebtAuctionType { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("invalid auction type %s", x)) + return + } + } + + if x := r.URL.Query().Get(RestDenom); len(x) != 0 { + auctionDenom = strings.TrimSpace(x) + err := sdk.ValidateDenom(auctionDenom) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + } + + if x := r.URL.Query().Get(RestPhase); len(x) != 0 { + auctionPhase = strings.ToLower(strings.TrimSpace(x)) + if auctionType != types.CollateralAuctionType && len(auctionType) > 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "cannot apply phase flag to non-collateral auction type") + return + } + if auctionPhase != types.ForwardAuctionPhase && auctionPhase != types.ReverseAuctionPhase { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("invalid auction phase %s", x)) + return + } + } + + params := types.NewQueryAllAuctionParams(page, limit, auctionType, auctionDenom, auctionPhase) + bz, err := cliCtx.Codec.MarshalJSON(params) if err != nil { - rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetAuctions) + res, height, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - // Decode and return results cliCtx = cliCtx.WithHeight(height) + // Unmarshal to Auction and remarshal as AuctionWithPhase var auctions types.Auctions err = cliCtx.Codec.UnmarshalJSON(res, &auctions) if err != nil { diff --git a/x/auction/client/rest/rest.go b/x/auction/client/rest/rest.go index 84eb85fd..4667f777 100644 --- a/x/auction/client/rest/rest.go +++ b/x/auction/client/rest/rest.go @@ -4,6 +4,16 @@ import ( "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" +) + +// REST Variable names +// nolint +const ( + RestType = "type" + RestDenom = "denom" + RestPhase = "phase" ) // RegisterRoutes - Central function to define routes that get registered by the main application @@ -11,3 +21,9 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { registerQueryRoutes(cliCtx, r) registerTxRoutes(cliCtx, r) } + +// placeBidReq defines the properties of a bid request's body +type placeBidReq struct { + BaseReq rest.BaseReq `json:"base_req"` + Amount sdk.Coin `json:"amount"` +} diff --git a/x/auction/client/rest/tx.go b/x/auction/client/rest/tx.go index 7e09366b..fcf171c2 100644 --- a/x/auction/client/rest/tx.go +++ b/x/auction/client/rest/tx.go @@ -18,14 +18,8 @@ func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/auctions/{%s}/bids", types.ModuleName, restAuctionID), bidHandlerFn(cliCtx)).Methods("POST") } -type placeBidReq struct { - BaseReq rest.BaseReq `json:"base_req"` - Amount sdk.Coin `json:"amount"` -} - func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Get auction ID from url auctionID, ok := rest.ParseUint64OrReturnBadRequest(w, mux.Vars(r)[restAuctionID]) if !ok { diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 6dc09704..9e7d9840 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -174,3 +174,12 @@ func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) } } } + +// GetAllAuctions returns all auctions from the store +func (k Keeper) GetAllAuctions(ctx sdk.Context) (auctions types.Auctions) { + k.IterateAuctions(ctx, func(auction types.Auction) bool { + auctions = append(auctions, auction) + return false + }) + return +} diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index e7d6460a..c65a0ef6 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -3,6 +3,7 @@ package keeper import ( abci "github.com/tendermint/tendermint/abci/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -50,15 +51,19 @@ func queryAuction(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte } func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) { - // Get all auctions - auctionsList := types.Auctions{} - keeper.IterateAuctions(ctx, func(a types.Auction) bool { - auctionsList = append(auctionsList, a) - return false - }) + var params types.QueryAllAuctionParams + err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } - // Encode Results - bz, err := codec.MarshalJSONIndent(keeper.cdc, auctionsList) + unfilteredAuctions := keeper.GetAllAuctions(ctx) + auctions := filterAuctions(ctx, unfilteredAuctions, params) + if auctions == nil { + auctions = types.Auctions{} + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, auctions) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } @@ -79,3 +84,41 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]by return bz, nil } + +// filterAuctions retrieves auctions filtered by a given set of params. +// If no filters are provided, all auctions will be returned in paginated form. +func filterAuctions(ctx sdk.Context, auctions types.Auctions, params types.QueryAllAuctionParams) types.Auctions { + filteredAuctions := make(types.Auctions, 0, len(auctions)) + + for _, auc := range auctions { + matchType, matchDenom, matchPhase := true, true, true + + // match auction type (if supplied) + if len(params.Type) > 0 { + matchType = auc.GetType() == params.Type + } + + // match auction denom (if supplied) + if len(params.Denom) > 0 { + matchDenom = auc.GetBid().Denom == params.Denom || auc.GetLot().Denom == params.Denom + } + + // match auction phase (if supplied) + if len(params.Phase) > 0 { + matchPhase = auc.GetPhase() == params.Phase + } + + if matchType && matchDenom && matchPhase { + filteredAuctions = append(filteredAuctions, auc) + } + } + + start, end := client.Paginate(len(filteredAuctions), params.Page, params.Limit, 100) + if start < 0 || end < 0 { + filteredAuctions = types.Auctions{} + } else { + filteredAuctions = filteredAuctions[start:end] + } + + return filteredAuctions +} diff --git a/x/auction/keeper/querier_test.go b/x/auction/keeper/querier_test.go index 20763bc8..8d74ee10 100644 --- a/x/auction/keeper/querier_test.go +++ b/x/auction/keeper/querier_test.go @@ -107,7 +107,9 @@ func (suite *QuerierTestSuite) TestQueryAuctions() { // Set up request query query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuctions}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + Data: types.ModuleCdc.MustMarshalJSON( + types.NewQueryAllAuctionParams(int(1), int(TestAuctionCount), "", "", ""), + ), } // Execute query and check the []byte result diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 7af97e6d..4c413913 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -10,6 +10,14 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" ) +const ( + CollateralAuctionType = "collateral" + SurplusAuctionType = "surplus" + DebtAuctionType = "debt" + ForwardAuctionPhase = "forward" + ReverseAuctionPhase = "reverse" +) + // DistantFuture is a very large time value to use as initial the ending time for auctions. // It is not set to the max time supported. This can cause problems with time comparisons, see https://stackoverflow.com/a/32620397. // Also amino panics when encoding times ≥ the start of year 10000. @@ -115,7 +123,7 @@ type SurplusAuction struct { func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } // GetType returns the auction type. Used to identify auctions in event attributes. -func (a SurplusAuction) GetType() string { return "surplus" } +func (a SurplusAuction) GetType() string { return SurplusAuctionType } // GetModuleAccountCoins returns the total number of coins held in the module account for this auction. // It is used in genesis initialize the module account correctly. @@ -125,7 +133,7 @@ func (a SurplusAuction) GetModuleAccountCoins() sdk.Coins { } // GetPhase returns the direction of a surplus auction, which never changes. -func (a SurplusAuction) GetPhase() string { return "forward" } +func (a SurplusAuction) GetPhase() string { return ForwardAuctionPhase } // NewSurplusAuction returns a new surplus auction. func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction { @@ -154,7 +162,7 @@ type DebtAuction struct { func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } // GetType returns the auction type. Used to identify auctions in event attributes. -func (a DebtAuction) GetType() string { return "debt" } +func (a DebtAuction) GetType() string { return DebtAuctionType } // GetModuleAccountCoins returns the total number of coins held in the module account for this auction. // It is used in genesis initialize the module account correctly. @@ -165,7 +173,7 @@ func (a DebtAuction) GetModuleAccountCoins() sdk.Coins { } // GetPhase returns the direction of a debt auction, which never changes. -func (a DebtAuction) GetPhase() string { return "reverse" } +func (a DebtAuction) GetPhase() string { return ReverseAuctionPhase } // Validate validates the DebtAuction fields values. func (a DebtAuction) Validate() error { @@ -213,7 +221,7 @@ type CollateralAuction struct { func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } // GetType returns the auction type. Used to identify auctions in event attributes. -func (a CollateralAuction) GetType() string { return "collateral" } +func (a CollateralAuction) GetType() string { return CollateralAuctionType } // GetModuleAccountCoins returns the total number of coins held in the module account for this auction. // It is used in genesis initialize the module account correctly. @@ -231,9 +239,9 @@ func (a CollateralAuction) IsReversePhase() bool { // GetPhase returns the direction of a collateral auction. func (a CollateralAuction) GetPhase() string { if a.IsReversePhase() { - return "reverse" + return ReverseAuctionPhase } - return "forward" + return ForwardAuctionPhase } // Validate validates the CollateralAuction fields values. diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index cf84514e..48721fdb 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -16,15 +16,21 @@ type QueryAuctionParams struct { // QueryAllAuctionParams is the params for an auctions query type QueryAllAuctionParams struct { - Page int `json:"page" yaml:"page"` - Limit int `json:"limit" yaml:"limit"` + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` + Type string `json:"type" yaml:"type"` + Denom string `json:"denom" yaml:"denom"` + Phase string `json:"phase" yaml:"phase"` } // NewQueryAllAuctionParams creates a new QueryAllAuctionParams -func NewQueryAllAuctionParams(page int, limit int) QueryAllAuctionParams { +func NewQueryAllAuctionParams(page, limit int, aucType, aucDenom, aucPhase string) QueryAllAuctionParams { return QueryAllAuctionParams{ Page: page, Limit: limit, + Type: aucType, + Denom: aucDenom, + Phase: aucPhase, } } diff --git a/x/bep3/keeper/querier.go b/x/bep3/keeper/querier.go index 3483a086..da1b0a25 100644 --- a/x/bep3/keeper/querier.go +++ b/x/bep3/keeper/querier.go @@ -124,7 +124,7 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]by return bz, nil } -// getAtomicSwapsFiltered retrieves atomic swaps filtered by a given set of params. +// filterAtomicSwaps retrieves atomic swaps filtered by a given set of params. // If no filters are provided, all atomic swaps will be returned in paginated form. func filterAtomicSwaps(ctx sdk.Context, swaps types.AtomicSwaps, params types.QueryAtomicSwaps) types.AtomicSwaps { filteredSwaps := make(types.AtomicSwaps, 0, len(swaps))