Auction cli/rest fixes (#319)

* cleanup auction tx cli

* add querier for getting one auction

* missed querier fixes

* update querier tests

* add msg struct tags

* add auction rest endpoint and tidy

* add struct tags to auctions

* minor UX tweaks
This commit is contained in:
Ruaridh 2020-01-21 17:41:37 +00:00 committed by GitHub
parent 2be1a3196e
commit f01a3f46ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 219 additions and 68 deletions

View File

@ -75,6 +75,5 @@ type (
GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid
Params = types.Params
QueryResAuctions = types.QueryResAuctions
Keeper = keeper.Keeper
)

View File

@ -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)
},
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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())

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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