diff --git a/x/auction/alias.go b/x/auction/alias.go index dc79f6fa..c0b7a522 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -75,6 +75,5 @@ type ( GenesisState = types.GenesisState MsgPlaceBid = types.MsgPlaceBid Params = types.Params - QueryResAuctions = types.QueryResAuctions Keeper = keeper.Keeper ) diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go index bb3d47a1..96bc15b7 100644 --- a/x/auction/client/cli/query.go +++ b/x/auction/client/cli/query.go @@ -2,12 +2,14 @@ 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/codec" + "github.com/kava-labs/kava/x/auction/types" ) @@ -20,6 +22,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } auctionQueryCmd.AddCommand(client.GetCommands( + QueryGetAuctionCmd(queryRoute, cdc), QueryGetAuctionsCmd(queryRoute, cdc), QueryParamsCmd(queryRoute, cdc), )...) @@ -27,24 +30,60 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return auctionQueryCmd } +// QueryGetAuctionCmd queries one auction in the store +func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "auction [auction-id]", + Short: "get a info about an auction", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + id, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("auction-id '%s' not a valid uint", args[0]) + } + bz, err := cdc.MarshalJSON(types.QueryAuctionParams{ + AuctionID: id, + }) + if err != nil { + return err + } + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuction), bz) + if err != nil { + return err + } + + // Decode and print results + var auction types.Auction + cdc.MustUnmarshalJSON(res, &auction) + return cliCtx.PrintOutput(auction) + }, + } +} + // QueryGetAuctionsCmd queries the auctions in the store func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "auctions", Short: "get a list of active auctions", + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) - res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/auctions", queryRoute), nil) + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuctions), nil) if err != nil { - fmt.Printf("error when getting auctions - %s", err) - return nil + return err } - var out types.QueryResAuctions - cdc.MustUnmarshalJSON(res, &out) - if len(out) == 0 { - out = append(out, "There are currently no auctions") - } - return cliCtx.PrintOutput(out) + + // Decode and print results + var auctions types.Auctions + cdc.MustUnmarshalJSON(res, &auctions) + return cliCtx.PrintOutput(auctions) }, } } diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go index b0adc7f0..4f143d7a 100644 --- a/x/auction/client/cli/tx.go +++ b/x/auction/client/cli/tx.go @@ -3,20 +3,22 @@ package cli import ( "fmt" "strconv" + "strings" - "github.com/kava-labs/kava/x/auction/types" "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" "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/version" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/kava-labs/kava/x/auction/types" ) // GetTxCmd returns the transaction commands for this module -// TODO: Tests, see: https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/cli/tx_test.go func GetTxCmd(cdc *codec.Codec) *cobra.Command { auctionTxCmd := &cobra.Command{ Use: "auction", @@ -33,22 +35,26 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { // GetCmdPlaceBid cli command for placing bids on auctions func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "placebid [auction-id] [amount]", + Use: "bid [auction-id] [amount]", Short: "place a bid on an auction", - Args: cobra.MinimumNArgs(2), + Long: strings.TrimSpace( + fmt.Sprintf(`Place a bid on any type of auction, updating the latest bid amount to [amount]. Collateral auctions must be bid up to their maxbid before entering reverse phase. + +Example: +$ %s tx %s bid 34 1000usdx --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) id, err := strconv.ParseUint(args[0], 10, 64) if err != nil { - fmt.Printf("invalid auction id - %s \n", string(args[0])) - return err + return fmt.Errorf("auction-id '%s' not a valid uint", args[0]) } - amt, err := sdk.ParseCoin(args[2]) + amt, err := sdk.ParseCoin(args[1]) if err != nil { - fmt.Printf("invalid amount - %s \n", string(args[2])) return err } diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go index f64a80bb..3a315445 100644 --- a/x/auction/client/rest/query.go +++ b/x/auction/client/rest/query.go @@ -12,18 +12,66 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) +const RestAuctionID = "auction-id" + func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc(fmt.Sprintf("/auction/getauctions"), queryGetAuctionsHandlerFn(cliCtx)).Methods("GET") - r.HandleFunc("/auction/params", getParamsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/auction/auctions/{%s}", RestAuctionID), queryAuctionHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/auction/auctions", queryAuctionsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/auction/parameters", getParamsHandlerFn(cliCtx)).Methods("GET") } -func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func queryAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - res, height, err := cliCtx.QueryWithData("/custom/auction/getauctions", nil) + // 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[RestAuctionID]) == 0 { + err := fmt.Errorf("%s required but not specified", RestAuctionID) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + auctionID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestAuctionID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.QueryAuctionParams{AuctionID: auctionID}) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData("custom/gov/proposal", bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Decode and return results + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryAuctionsHandlerFn(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 + } + // Get all auctions + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/auction/%s", types.QueryGetAuctions), nil) if err != nil { rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return } + // Return auctions cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) } @@ -31,14 +79,19 @@ func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func getParamsHandlerFn(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 + } // Get the params res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/auction/%s", types.QueryGetParams), nil) - cliCtx = cliCtx.WithHeight(height) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } // Return the params + cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) } } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 0c69c231..b8079c20 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -250,7 +250,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc if err != nil { return a, err } - // Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement. + // Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement (or whatever is left if < bidIncrement). if a.CorrespondingDebt.IsPositive() { debtAmountToReturn := sdk.MinInt(bidIncrement.Amount, a.CorrespondingDebt.Amount) @@ -367,7 +367,7 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac return a, err } } - // Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to Bid. + // Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to min of Bid and amount of debt. if a.Bidder.Equals(supply.NewModuleAddress(a.Initiator)) { debtAmountToReturn := sdk.MinInt(a.Bid.Amount, a.CorrespondingDebt.Amount) diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 29bfebc9..08d337b3 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -3,8 +3,9 @@ package keeper import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/auction/types" abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/auction/types" ) // NewQuerier is the module level router for state queries @@ -12,6 +13,8 @@ func NewQuerier(keeper Keeper) sdk.Querier { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { switch path[0] { case types.QueryGetAuction: + return queryAuction(ctx, req, keeper) + case types.QueryGetAuctions: return queryAuctions(ctx, req, keeper) case types.QueryGetParams: return queryGetParams(ctx, req, keeper) @@ -21,17 +24,41 @@ func NewQuerier(keeper Keeper) sdk.Querier { } } -func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var auctionsList types.Auctions +func queryAuction(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Decode request + var requestParams types.QueryAuctionParams + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + // Lookup auction + auction, found := keeper.GetAuction(ctx, requestParams.AuctionID) + if !found { + return nil, types.ErrAuctionNotFound(types.DefaultCodespace, requestParams.AuctionID) + } + + // Encode results + bz, err := codec.MarshalJSONIndent(keeper.cdc, auction) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return bz, nil +} + +func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Get all auctions + auctionsList := types.Auctions{} keeper.IterateAuctions(ctx, func(a types.Auction) bool { auctionsList = append(auctionsList, a) return false }) - bz, err2 := codec.MarshalJSONIndent(keeper.cdc, auctionsList) - if err2 != nil { - return nil, sdk.ErrInternal("could not marshal result to JSON") + // Encode Results + bz, err := codec.MarshalJSONIndent(keeper.cdc, auctionsList) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } return bz, nil @@ -47,5 +74,6 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]by if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } + return bz, nil } diff --git a/x/auction/keeper/querier_test.go b/x/auction/keeper/querier_test.go index c1ec1f7f..8ecf15ec 100644 --- a/x/auction/keeper/querier_test.go +++ b/x/auction/keeper/querier_test.go @@ -59,10 +59,11 @@ func (suite *QuerierTestSuite) SetupTest() { suite.keeper = tApp.GetAuctionKeeper() // Populate with auctions + randSrc := rand.New(rand.NewSource(int64(1234))) for j := 0; j < TestAuctionCount; j++ { - lotAmount := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 10, 100) + lotAmount := simulation.RandIntBetween(randSrc, 10, 100) id, err := suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", int64(lotAmount)), "token2") - suite.Nil(err) + suite.NoError(err) auc, found := suite.keeper.GetAuction(suite.ctx, id) suite.True(found) @@ -72,12 +73,12 @@ func (suite *QuerierTestSuite) SetupTest() { suite.querier = keeper.NewQuerier(suite.keeper) } -func (suite *QuerierTestSuite) TestQueryAuctions() { +func (suite *QuerierTestSuite) TestQueryAuction() { ctx := suite.ctx.WithIsCheckTx(false) // Set up request query query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuction}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + Data: types.ModuleCdc.MustMarshalJSON(types.QueryAuctionParams{AuctionID: 0}), // get the first auction } // Execute query and check the []byte result @@ -85,11 +86,40 @@ func (suite *QuerierTestSuite) TestQueryAuctions() { suite.NoError(err) suite.NotNil(bz) + // Unmarshal the bytes into type Auction + var auction types.Auction + suite.NoError(types.ModuleCdc.UnmarshalJSON(bz, &auction)) + + // Check the returned auction + suite.Equal(suite.auctions[0].GetID(), auction.GetID()) + suite.Equal(suite.auctions[0].GetInitiator(), auction.GetInitiator()) + suite.Equal(suite.auctions[0].GetLot(), auction.GetLot()) + suite.Equal(suite.auctions[0].GetBid(), auction.GetBid()) + suite.Equal(suite.auctions[0].GetEndTime(), auction.GetEndTime()) + +} + +func (suite *QuerierTestSuite) TestQueryAuctions() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuctions}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryGetAuctions}, query) + suite.NoError(err) + suite.NotNil(bz) + // Unmarshal the bytes into type Auctions var auctions types.Auctions - suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &auctions)) + suite.NoError(types.ModuleCdc.UnmarshalJSON(bz, &auctions)) // Check that each Auction has correct values + if len(auctions) == 0 && len(suite.auctions) != 0 { + suite.FailNow("no auctions returned") // skip the panic from indexing empty slice below + } for i := 0; i < TestAuctionCount; i++ { suite.Equal(suite.auctions[i].GetID(), auctions[i].GetID()) suite.Equal(suite.auctions[i].GetInitiator(), auctions[i].GetInitiator()) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 3ef09f47..583d9765 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -29,14 +29,14 @@ type Auctions []Auction // BaseAuction is a common type shared by all Auctions. type BaseAuction struct { - ID uint64 - Initiator string // Module name that starts the auction. Pays out Lot. - Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. - Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. - Bid sdk.Coin // Coins paid into the auction the bidder. - HasReceivedBids bool // Whether the auction has received any bids or not. - EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. - MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. + ID uint64 `json:"id" yaml:"id"` + Initiator string `json:"initiator" yaml:"initiator"` // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin `json:"lot" yaml:"lot"` // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress `json:"bidder" yaml:"bidder"` // Latest bidder. Receiver of Lot. + Bid sdk.Coin `json:"bid" yaml:"bid"` // Coins paid into the auction the bidder. + HasReceivedBids bool `json:"has_received_bids" yaml:"has_received_bids"` // Whether the auction has received any bids or not. + EndTime time.Time `json:"end_time" yaml:"end_time"` // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. + MaxEndTime time.Time `json:"max_end_time" yaml:"max_end_time"` // Maximum closing time. Auctions can close before this but never after. } // GetID is a getter for auction ID. @@ -82,7 +82,7 @@ func (a BaseAuction) String() string { // SurplusAuction is a forward auction that burns what it receives from bids. // It is normally used to sell off excess pegged asset acquired by the CDP system. type SurplusAuction struct { - BaseAuction + BaseAuction `json:"base_auction" yaml:"base_auction"` } // WithID returns an auction with the ID set. @@ -116,8 +116,8 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // DebtAuction is a reverse auction that mints what it pays out. // It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { - BaseAuction - CorrespondingDebt sdk.Coin + BaseAuction `json:"base_auction" yaml:"base_auction"` + CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"` } // WithID returns an auction with the ID set. @@ -160,10 +160,10 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, e // Unsold Lot is sent to LotReturns, being divided among the addresses by weight. // Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { - BaseAuction - CorrespondingDebt sdk.Coin - MaxBid sdk.Coin - LotReturns WeightedAddresses + BaseAuction `json:"base_auction" yaml:"base_auction"` + CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"` + MaxBid sdk.Coin `json:"max_bid" yaml:"max_bid"` + LotReturns WeightedAddresses `json:"lot_returns" yaml:"lot_returns"` } // WithID returns an auction with the ID set. diff --git a/x/auction/types/errors.go b/x/auction/types/errors.go index f5b427aa..d281f3aa 100644 --- a/x/auction/types/errors.go +++ b/x/auction/types/errors.go @@ -68,7 +68,7 @@ func ErrInvalidLotDenom(codespace sdk.CodespaceType, lotDenom string, auctionLot // ErrBidTooSmall error for when bid is not greater than auction's last bid func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, lastBid sdk.Coin) sdk.Error { - return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is smaller than auction's last bid %s", bid.String(), lastBid.String())) + return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is not greater than auction's last bid %s", bid.String(), lastBid.String())) } // ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index ed18d84b..b60fbbf4 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -7,9 +7,9 @@ var _ sdk.Msg = &MsgPlaceBid{} // MsgPlaceBid is the message type used to place a bid on any type of auction. type MsgPlaceBid struct { - AuctionID uint64 - Bidder sdk.AccAddress - Amount sdk.Coin // The new bid or lot to be set on the auction. + AuctionID uint64 `json:"auction_id" yaml:"auction_id"` + Bidder sdk.AccAddress `json:"bidder" yaml:"bidder"` + Amount sdk.Coin `json:"amount" yaml:"amount"` // The new bid or lot to be set on the auction. } // NewMsgPlaceBid returns a new MsgPlaceBid. diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 038e9bb0..c8019f10 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -1,21 +1,17 @@ package types -import ( - "strings" -) - const ( - // QueryGetAuction command for getting the information about a particular auction - QueryGetAuction = "auctions" - QueryGetParams = "params" + // QueryGetAuction is the query path for querying one auction + QueryGetAuction = "auction" + // QueryGetAuction is the query path for querying all auctions + QueryGetAuctions = "auctions" + // QueryGetAuction is the query path for querying the global auction params + QueryGetParams = "params" ) -// QueryResAuctions Result Payload for an auctions query -type QueryResAuctions []string - -// implement fmt.Stringer -func (n QueryResAuctions) String() string { - return strings.Join(n[:], "\n") +// QueryAuctionParams params for query /auction/auction +type QueryAuctionParams struct { + AuctionID uint64 } // QueryAllAuctionParams is the params for an auctions query