From 70c7539b5872465b09de4ef62053510d8797b799 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Mon, 2 Oct 2023 16:32:50 -0600 Subject: [PATCH] [1658]: Implement QueryValidateCreateMarket and QueryValidateMarket. --- x/exchange/keeper/grpc_query.go | 64 ++++++++++++++++- x/exchange/keeper/keeper.go | 5 ++ x/exchange/keeper/market.go | 61 +++++++++++++--- x/exchange/keeper/msg_server.go | 7 -- x/exchange/market.go | 12 ++++ x/exchange/market_test.go | 121 ++++++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+), 19 deletions(-) diff --git a/x/exchange/keeper/grpc_query.go b/x/exchange/keeper/grpc_query.go index c715dd6870..4d70abf18e 100644 --- a/x/exchange/keeper/grpc_query.go +++ b/x/exchange/keeper/grpc_query.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "errors" "fmt" "google.golang.org/grpc/codes" @@ -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. diff --git a/x/exchange/keeper/keeper.go b/x/exchange/keeper/keeper.go index e2110472db..44c090bb68 100644 --- a/x/exchange/keeper/keeper.go +++ b/x/exchange/keeper/keeper.go @@ -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 diff --git a/x/exchange/keeper/market.go b/x/exchange/keeper/market.go index 5c97b1d790..aa5ee2059a 100644 --- a/x/exchange/keeper/market.go +++ b/x/exchange/keeper/market.go @@ -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 @@ -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) @@ -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...) +} diff --git a/x/exchange/keeper/msg_server.go b/x/exchange/keeper/msg_server.go index 12117997ad..75e5e499be 100644 --- a/x/exchange/keeper/msg_server.go +++ b/x/exchange/keeper/msg_server.go @@ -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" ) @@ -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) { diff --git a/x/exchange/market.go b/x/exchange/market.go index 78bac15c43..4ce0b8aa3a 100644 --- a/x/exchange/market.go +++ b/x/exchange/market.go @@ -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 diff --git a/x/exchange/market_test.go b/x/exchange/market_test.go index 1d0524b568..05f73488c5 100644 --- a/x/exchange/market_test.go +++ b/x/exchange/market_test.go @@ -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")