Skip to content

Commit

Permalink
[1658]: Implement QueryValidateCreateMarket and QueryValidateMarket.
Browse files Browse the repository at this point in the history
  • Loading branch information
SpicyLemon committed Oct 2, 2023
1 parent 22f7129 commit 70c7539
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 19 deletions.
64 changes: 62 additions & 2 deletions x/exchange/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"context"
"errors"
"fmt"

"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -253,8 +254,67 @@ func (k QueryServer) QueryParams(goCtx context.Context, _ *exchange.QueryParamsR

// QueryValidateCreateMarket checks the provided MsgGovCreateMarketResponse and returns any errors it might have.
func (k QueryServer) QueryValidateCreateMarket(goCtx context.Context, req *exchange.QueryValidateCreateMarketRequest) (*exchange.QueryValidateCreateMarketResponse, error) {
// TODO[1658]: Implement QueryValidateCreateMarket query
panic("not implemented")
if req == nil || req.CreateMarketRequest == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

msg := req.CreateMarketRequest
resp := &exchange.QueryValidateCreateMarketResponse{}

if err := msg.ValidateBasic(); err != nil {
resp.Error = err.Error()
return resp, nil
}

if msg.Authority != k.authority {
resp.Error = k.wrongAuthErr(msg.Authority).Error()
return resp, nil
}

// The SDK *should* already be using a cache context for queries, but I'm doing it here too just to be on the safe side.
ctx, _ := sdk.UnwrapSDKContext(goCtx).CacheContext()
var marketID uint32
if newMarketID, err := k.CreateMarket(ctx, msg.Market); err != nil {
resp.Error = err.Error()
return resp, nil
} else {
marketID = newMarketID
}

resp.GovPropWillPass = true

var errs []error
if err := exchange.ValidateReqAttrsAreNormalized("create ask", msg.Market.ReqAttrCreateAsk); err != nil {
errs = append(errs, err)
}
if err := exchange.ValidateReqAttrsAreNormalized("create bid", msg.Market.ReqAttrCreateBid); err != nil {
errs = append(errs, err)
}

if err := k.ValidateMarket(ctx, marketID); err != nil {
errs = append(errs, err)
}

if len(errs) > 0 {
resp.Error = errors.Join(errs...).Error()
}

return resp, nil
}

// QueryValidateMarket checks for any problems with a market's setup.
func (k QueryServer) QueryValidateMarket(goCtx context.Context, req *exchange.QueryValidateMarketRequest) (*exchange.QueryValidateMarketResponse, error) {
if req == nil || req.MarketId == 0 {
return nil, status.Error(codes.InvalidArgument, "empty request")
}

ctx := sdk.UnwrapSDKContext(goCtx)
resp := &exchange.QueryValidateMarketResponse{}
if err := k.ValidateMarket(ctx, req.MarketId); err != nil {
resp.Error = err.Error()
}

return resp, nil
}

// QueryValidateManageFees checks the provided MsgGovManageFeesRequest and returns any errors that it might have.
Expand Down
5 changes: 5 additions & 0 deletions x/exchange/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (k Keeper) logErrorf(ctx sdk.Context, msg string, args ...interface{}) {
ctx.Logger().Error(fmt.Sprintf(msg, args...), "module", "x/"+exchange.ModuleName)
}

// wrongAuthErr returns the error to use when a message's authority isn't what's required.
func (k Keeper) wrongAuthErr(badAuthority string) error {
return govtypes.ErrInvalidSigner.Wrapf("expected %s got %s", k.GetAuthority(), badAuthority)
}

// GetAuthority gets the address (as bech32) that has governance authority.
func (k Keeper) GetAuthority() string {
return k.authority
Expand Down
61 changes: 51 additions & 10 deletions x/exchange/keeper/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -873,10 +873,11 @@ func (k Keeper) NextMarketID(ctx sdk.Context) uint32 {
store := k.getStore(ctx)
marketID := getLastAutoMarketID(store) + 1
for {
marketAddr := exchange.GetMarketAddress(marketID)
if !k.accountKeeper.HasAccount(ctx, marketAddr) {
key := MakeKeyKnownMarketID(marketID)
if !store.Has(key) {
break
}
marketID++
}
setLastAutoMarketID(store, marketID)
return marketID
Expand Down Expand Up @@ -945,14 +946,7 @@ func storeMarket(store sdk.KVStore, market exchange.Market) {

// CreateMarket saves a new market to the store with all the info provided.
// If the marketId is zero, the next available one will be used.
func (k Keeper) CreateMarket(ctx sdk.Context, market exchange.Market) (marketID uint32, err error) {
defer func() {
// TODO[1658]: Figure out why this recover is needed and either add a comment or delete this defer.
if r := recover(); r != nil {
err = fmt.Errorf("could not create market: %v", r)
}
}()

func (k Keeper) CreateMarket(ctx sdk.Context, market exchange.Market) (uint32, error) {
// Note: The Market is passed in by value, so any alterations to it here will be lost upon return.
var errAsk, errBid error
market.ReqAttrCreateAsk, errAsk = exchange.NormalizeReqAttrs(market.ReqAttrCreateAsk)
Expand Down Expand Up @@ -1262,3 +1256,50 @@ func (k Keeper) UpdateReqAttrs(ctx sdk.Context, msg *exchange.MsgMarketManageReq

return ctx.EventManager().EmitTypedEvent(exchange.NewEventMarketReqAttrUpdated(marketID, admin))
}

// ValidateMarket checks the setup of the provided market, making sure there aren't any possibly problematic settings.
func (k Keeper) ValidateMarket(ctx sdk.Context, marketID uint32) error {
store := k.getStore(ctx)
if err := validateMarketExists(store, marketID); err != nil {
return err
}

var errs []error

sellerRatios := getSellerSettlementRatios(store, marketID)
buyerRatios := getBuyerSettlementRatios(store, marketID)
if len(sellerRatios) > 0 && len(buyerRatios) > 0 {
// We only need to check the price denoms if *both* types have an entry.
sellerPriceDenoms := make([]string, len(sellerRatios))
sellerPriceDenomsKnown := make(map[string]bool)
for i, ratio := range sellerRatios {
sellerPriceDenoms[i] = ratio.Price.Denom
sellerPriceDenomsKnown[ratio.Price.Denom] = true
}

buyerPriceDenoms := make([]string, 0, len(sellerRatios))
buyerPriceDenomsKnown := make(map[string]bool)
for _, ratio := range buyerRatios {
if !buyerPriceDenomsKnown[ratio.Price.Denom] {
buyerPriceDenoms = append(buyerPriceDenoms, ratio.Price.Denom)
buyerPriceDenomsKnown[ratio.Price.Denom] = true
}
}

for _, denom := range sellerPriceDenoms {
if !buyerPriceDenomsKnown[denom] {
errs = append(errs, fmt.Errorf("seller settlement fee ratios have price denom %q "+
"but there are no buyer settlement fee ratios with that price denom", denom))
}
}

for _, denom := range buyerPriceDenoms {
if !sellerPriceDenomsKnown[denom] {
errs = append(errs, fmt.Errorf("buyer settlement fee ratios have price denom %q "+
"but there is not a seller settlement fee ratio with that price denom", denom))
}
}
}

return errors.Join(errs...)
}
7 changes: 0 additions & 7 deletions x/exchange/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ 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"

"github.com/provenance-io/provenance/x/exchange"
)

Expand Down Expand Up @@ -174,11 +172,6 @@ func (k MsgServer) MarketManageReqAttrs(goCtx context.Context, msg *exchange.Msg
return &exchange.MsgMarketManageReqAttrsResponse{}, nil
}

// wrongAuthErr returns the error to use when a message's authority isn't what's required.
func (k MsgServer) wrongAuthErr(badAuthority string) error {
return govtypes.ErrInvalidSigner.Wrapf("expected %s got %s", k.GetAuthority(), badAuthority)
}

// GovCreateMarket is a governance proposal endpoint for creating a market.
func (k MsgServer) GovCreateMarket(goCtx context.Context, msg *exchange.MsgGovCreateMarketRequest) (*exchange.MsgGovCreateMarketResponse, error) {
if !k.IsAuthority(msg.Authority) {
Expand Down
12 changes: 12 additions & 0 deletions x/exchange/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,18 @@ func NormalizeReqAttrs(reqAttrs []string) ([]string, error) {
return rv, errors.Join(errs...)
}

// ValidateReqAttrsAreNormalized checks that each of the provided attrs is equal to its normalized version.
func ValidateReqAttrsAreNormalized(field string, attrs []string) error {
var errs []error
for _, attr := range attrs {
norm := nametypes.NormalizeName(attr)
if attr != norm {
errs = append(errs, fmt.Errorf("%s required attribute %q is not normalized, expected %q", field, attr, norm))
}
}
return errors.Join(errs...)
}

// ValidateReqAttrs makes sure that each provided attribute is valid and that no duplicate entries are provided.
func ValidateReqAttrs(field string, attrs []string) error {
var errs []error
Expand Down
121 changes: 121 additions & 0 deletions x/exchange/market_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2506,6 +2506,127 @@ func TestNormalizeReqAttrs(t *testing.T) {
}
}

func TestValidateReqAttrsAreNormalized(t *testing.T) {
joinErrs := func(errs ...string) string {
return strings.Join(errs, "\n")
}
notNormErr := func(field, attr, norm string) string {
return fmt.Sprintf("%s required attribute %q is not normalized, expected %q", field, attr, norm)
}

tests := []struct {
name string
field string
attrs []string
expErr string
}{
{name: "nil attrs", field: "FOILD", attrs: nil, expErr: ""},
{name: "empty attrs", field: "FOILD", attrs: []string{}, expErr: ""},
{
name: "one attr: normalized",
field: "TINFOILD",
attrs: []string{"abc.def"},
expErr: "",
},
{
name: "one attr: with whitespace",
field: "AlFOILD",
attrs: []string{" abc.def"},
expErr: notNormErr("AlFOILD", " abc.def", "abc.def"),
},
{
name: "one attr: with upper",
field: "AlFOILD",
attrs: []string{"aBc.def"},
expErr: notNormErr("AlFOILD", "aBc.def", "abc.def"),
},
{
name: "one attr: with wildcard, ok",
field: "NOFOILD",
attrs: []string{"*.abc.def"},
expErr: "",
},
{
name: "one attr: with wildcard, bad",
field: "AirFOILD",
attrs: []string{"*.abc. def"},
expErr: notNormErr("AirFOILD", "*.abc. def", "*.abc.def"),
},
{
name: "three attrs: all okay",
field: "WhaFoild",
attrs: []string{"abc.def", "*.ghi.jkl", "mno.pqr.stu.vwx.yz"},
expErr: "",
},
{
name: "three attrs: bad first",
field: "Uno1Foild",
attrs: []string{"abc. def", "*.ghi.jkl", "mno.pqr.stu.vwx.yz"},
expErr: notNormErr("Uno1Foild", "abc. def", "abc.def"),
},
{
name: "three attrs: bad second",
field: "Uno2Foild",
attrs: []string{"abc.def", "*.ghi.jkl ", "mno.pqr.stu.vwx.yz"},
expErr: notNormErr("Uno2Foild", "*.ghi.jkl ", "*.ghi.jkl"),
},
{
name: "three attrs: bad third",
field: "Uno3Foild",
attrs: []string{"abc.def", "*.ghi.jkl", "mnO.pqr.stu.vwX.yz"},
expErr: notNormErr("Uno3Foild", "mnO.pqr.stu.vwX.yz", "mno.pqr.stu.vwx.yz"),
},
{
name: "three attrs: bad first and second",
field: "TwoFold1",
attrs: []string{"abc.Def", "* .ghi.jkl", "mno.pqr.stu.vwx.yz"},
expErr: joinErrs(
notNormErr("TwoFold1", "abc.Def", "abc.def"),
notNormErr("TwoFold1", "* .ghi.jkl", "*.ghi.jkl"),
),
},
{
name: "three attrs: bad first and third",
field: "TwoFold2",
attrs: []string{"abc . def", "*.ghi.jkl", "mno.pqr. stu .vwx.yz"},
expErr: joinErrs(
notNormErr("TwoFold2", "abc . def", "abc.def"),
notNormErr("TwoFold2", "mno.pqr. stu .vwx.yz", "mno.pqr.stu.vwx.yz"),
),
},
{
name: "three attrs: bad second and third",
field: "TwoFold3",
attrs: []string{"abc.def", "*.ghi.JKl", "mno.pqr.sTu.vwx.yz"},
expErr: joinErrs(
notNormErr("TwoFold3", "*.ghi.JKl", "*.ghi.jkl"),
notNormErr("TwoFold3", "mno.pqr.sTu.vwx.yz", "mno.pqr.stu.vwx.yz"),
),
},
{
name: "three attrs: all bad",
field: "CURSES!",
attrs: []string{" abc . def ", " * . ghi . jkl ", " mno . pqr . stu . vwx . yz "},
expErr: joinErrs(
notNormErr("CURSES!", " abc . def ", "abc.def"),
notNormErr("CURSES!", " * . ghi . jkl ", "*.ghi.jkl"),
notNormErr("CURSES!", " mno . pqr . stu . vwx . yz ", "mno.pqr.stu.vwx.yz"),
),
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var err error
testFunc := func() {
err = ValidateReqAttrsAreNormalized(tc.field, tc.attrs)
}
require.NotPanics(t, testFunc, "ValidateReqAttrsAreNormalized")
assertions.AssertErrorValue(t, err, tc.expErr, "ValidateReqAttrsAreNormalized error")
})
}
}

func TestValidateReqAttrs(t *testing.T) {
joinErrs := func(errs ...string) string {
return strings.Join(errs, "\n")
Expand Down

0 comments on commit 70c7539

Please sign in to comment.