diff --git a/server/v2/CHANGELOG.md b/server/v2/CHANGELOG.md index f5663b4f8b21..b5f51845219e 100644 --- a/server/v2/CHANGELOG.md +++ b/server/v2/CHANGELOG.md @@ -22,6 +22,10 @@ Each entry must include the Github issue reference in the following format: ## [Unreleased] +### Features + +* [#22715](https://github.com/cosmos/cosmos-sdk/pull/22941) Add custom HTTP handler for grpc-gateway that removes the need to manually register grpc-gateway services. + ## [v2.0.0-beta.1](https://github.com/cosmos/cosmos-sdk/releases/tag/server/v2.0.0-beta.1) Initial tag of `cosmossdk.io/server/v2`. diff --git a/server/v2/api/grpcgateway/interceptor.go b/server/v2/api/grpcgateway/interceptor.go new file mode 100644 index 000000000000..7119d452a9b9 --- /dev/null +++ b/server/v2/api/grpcgateway/interceptor.go @@ -0,0 +1,145 @@ +package grpcgateway + +import ( + "net/http" + "strconv" + "strings" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "cosmossdk.io/server/v2/appmanager" +) + +var _ http.Handler = &gatewayInterceptor[transaction.Tx]{} + +// gatewayInterceptor handles routing grpc-gateway queries to the app manager's query router. +type gatewayInterceptor[T transaction.Tx] struct { + logger log.Logger + // gateway is the fallback grpc gateway mux handler. + gateway *runtime.ServeMux + + // customEndpointMapping is a mapping of custom GET options on proto RPC handlers, to the fully qualified method name. + // + // example: /cosmos/bank/v1beta1/denoms_metadata -> cosmos.bank.v1beta1.Query.DenomsMetadata + customEndpointMapping map[string]string + + // appManager is used to route queries to the application. + appManager appmanager.AppManager[T] +} + +// newGatewayInterceptor creates a new gatewayInterceptor. +func newGatewayInterceptor[T transaction.Tx](logger log.Logger, gateway *runtime.ServeMux, am appmanager.AppManager[T]) (*gatewayInterceptor[T], error) { + getMapping, err := getHTTPGetAnnotationMapping() + if err != nil { + return nil, err + } + return &gatewayInterceptor[T]{ + logger: logger, + gateway: gateway, + customEndpointMapping: getMapping, + appManager: am, + }, nil +} + +// ServeHTTP implements the http.Handler interface. This function will attempt to match http requests to the +// interceptors internal mapping of http annotations to query request type names. +// If no match can be made, it falls back to the runtime gateway server mux. +func (g *gatewayInterceptor[T]) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + g.logger.Debug("received grpc-gateway request", "request_uri", request.RequestURI) + match := matchURL(request.URL, g.customEndpointMapping) + if match == nil { + // no match cases fall back to gateway mux. + g.gateway.ServeHTTP(writer, request) + return + } + g.logger.Debug("matched request", "query_input", match.QueryInputName) + _, out := runtime.MarshalerForRequest(g.gateway, request) + var msg gogoproto.Message + var err error + + switch request.Method { + case http.MethodPost: + msg, err = createMessageFromJSON(match, request) + case http.MethodGet: + msg, err = createMessage(match) + default: + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, status.Error(codes.Unimplemented, "HTTP method must be POST or GET")) + return + } + if err != nil { + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + return + } + + // extract block height header + var height uint64 + heightStr := request.Header.Get(GRPCBlockHeightHeader) + if heightStr != "" { + height, err = strconv.ParseUint(heightStr, 10, 64) + if err != nil { + err = status.Errorf(codes.InvalidArgument, "invalid height: %s", heightStr) + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + return + } + } + + query, err := g.appManager.Query(request.Context(), height, msg) + if err != nil { + // if we couldn't find a handler for this request, just fall back to the gateway mux. + if strings.Contains(err.Error(), "no handler") { + g.gateway.ServeHTTP(writer, request) + } else { + // for all other errors, we just return the error. + runtime.DefaultHTTPProtoErrorHandler(request.Context(), g.gateway, out, writer, request, err) + } + return + } + // for no errors, we forward the response. + runtime.ForwardResponseMessage(request.Context(), g.gateway, out, writer, request, query) +} + +// getHTTPGetAnnotationMapping returns a mapping of RPC Method HTTP GET annotation to the RPC Handler's Request Input type full name. +// +// example: "/cosmos/auth/v1beta1/account_info/{address}":"cosmos.auth.v1beta1.Query.AccountInfo" +func getHTTPGetAnnotationMapping() (map[string]string, error) { + protoFiles, err := gogoproto.MergedRegistry() + if err != nil { + return nil, err + } + + httpGets := make(map[string]string) + protoFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + for i := 0; i < fd.Services().Len(); i++ { + serviceDesc := fd.Services().Get(i) + for j := 0; j < serviceDesc.Methods().Len(); j++ { + methodDesc := serviceDesc.Methods().Get(j) + + httpAnnotation := proto.GetExtension(methodDesc.Options(), annotations.E_Http) + if httpAnnotation == nil { + continue + } + + httpRule, ok := httpAnnotation.(*annotations.HttpRule) + if !ok || httpRule == nil { + continue + } + if httpRule.GetGet() == "" { + continue + } + + httpGets[httpRule.GetGet()] = string(methodDesc.Input().FullName()) + } + } + return true + }) + + return httpGets, nil +} diff --git a/server/v2/api/grpcgateway/server.go b/server/v2/api/grpcgateway/server.go index 977eb369c894..2aec6cad6387 100644 --- a/server/v2/api/grpcgateway/server.go +++ b/server/v2/api/grpcgateway/server.go @@ -14,6 +14,7 @@ import ( "cosmossdk.io/core/transaction" "cosmossdk.io/log" serverv2 "cosmossdk.io/server/v2" + "cosmossdk.io/server/v2/appmanager" ) var ( @@ -37,6 +38,7 @@ func New[T transaction.Tx]( logger log.Logger, config server.ConfigMap, ir jsonpb.AnyResolver, + appManager appmanager.AppManager[T], cfgOptions ...CfgOption, ) (*Server[T], error) { // The default JSON marshaller used by the gRPC-Gateway is unable to marshal non-nullable non-scalar fields. @@ -71,12 +73,14 @@ func New[T transaction.Tx]( } } - // TODO: register the gRPC-Gateway routes - s.logger = logger.With(log.ModuleKey, s.Name()) s.config = serverCfg mux := http.NewServeMux() - mux.Handle("/", s.GRPCGatewayRouter) + interceptor, err := newGatewayInterceptor[T](logger, s.GRPCGatewayRouter, appManager) + if err != nil { + return nil, fmt.Errorf("failed to create grpc-gateway interceptor: %w", err) + } + mux.Handle("/", interceptor) s.server = &http.Server{ Addr: s.config.Address, @@ -133,15 +137,15 @@ func (s *Server[T]) Stop(ctx context.Context) error { return s.server.Shutdown(ctx) } +// GRPCBlockHeightHeader is the gRPC header for block height. +const GRPCBlockHeightHeader = "x-cosmos-block-height" + // CustomGRPCHeaderMatcher for mapping request headers to // GRPC metadata. // HTTP headers that start with 'Grpc-Metadata-' are automatically mapped to // gRPC metadata after removing prefix 'Grpc-Metadata-'. We can use this // CustomGRPCHeaderMatcher if headers don't start with `Grpc-Metadata-` func CustomGRPCHeaderMatcher(key string) (string, bool) { - // GRPCBlockHeightHeader is the gRPC header for block height. - const GRPCBlockHeightHeader = "x-cosmos-block-height" - switch strings.ToLower(key) { case GRPCBlockHeightHeader: return GRPCBlockHeightHeader, true diff --git a/server/v2/api/grpcgateway/uri.go b/server/v2/api/grpcgateway/uri.go new file mode 100644 index 000000000000..6531447cf889 --- /dev/null +++ b/server/v2/api/grpcgateway/uri.go @@ -0,0 +1,187 @@ +package grpcgateway + +import ( + "io" + "net/http" + "net/url" + "reflect" + "regexp" + "strings" + + "github.com/cosmos/gogoproto/jsonpb" + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/mitchellh/mapstructure" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const maxBodySize = 1 << 20 // 1 MB + +// uriMatch contains information related to a URI match. +type uriMatch struct { + // QueryInputName is the fully qualified name of the proto input type of the query rpc method. + QueryInputName string + + // Params are any wildcard/query params found in the request. + // + // example: + // - foo/bar/{baz} - foo/bar/qux -> {baz: qux} + // - foo/bar?baz=qux - foo/bar -> {baz: qux} + Params map[string]string +} + +// HasParams reports whether the uriMatch has any params. +func (uri uriMatch) HasParams() bool { + return len(uri.Params) > 0 +} + +// matchURL attempts to find a match for the given URL. +// NOTE: if no match is found, nil is returned. +func matchURL(u *url.URL, getPatternToQueryInputName map[string]string) *uriMatch { + uriPath := strings.TrimRight(u.Path, "/") + queryParams := u.Query() + + params := make(map[string]string) + for key, vals := range queryParams { + if len(vals) > 0 { + // url.Values contains a slice for the values as you are able to specify a key multiple times in URL. + // example: https://localhost:9090/do/something?color=red&color=blue&color=green + // We will just take the first value in the slice. + params[key] = vals[0] + } + } + + // for simple cases where there are no wildcards, we can just do a map lookup. + if inputName, ok := getPatternToQueryInputName[uriPath]; ok { + return &uriMatch{ + QueryInputName: inputName, + Params: params, + } + } + + // attempt to find a match in the pattern map. + for getPattern, queryInputName := range getPatternToQueryInputName { + getPattern = strings.TrimRight(getPattern, "/") + + regexPattern, wildcardNames := patternToRegex(getPattern) + + regex := regexp.MustCompile(regexPattern) + matches := regex.FindStringSubmatch(uriPath) + + if len(matches) > 1 { + // first match is the full string, subsequent matches are capture groups + for i, name := range wildcardNames { + params[name] = matches[i+1] + } + + return &uriMatch{ + QueryInputName: queryInputName, + Params: params, + } + } + } + + return nil +} + +// patternToRegex converts a URI pattern with wildcards to a regex pattern. +// Returns the regex pattern and a slice of wildcard names in order +func patternToRegex(pattern string) (string, []string) { + escaped := regexp.QuoteMeta(pattern) + var wildcardNames []string + + // extract and replace {param=**} patterns + r1 := regexp.MustCompile(`\\\{([^}]+?)=\\\*\\\*\\}`) + escaped = r1.ReplaceAllStringFunc(escaped, func(match string) string { + // extract wildcard name without the =** suffix + name := regexp.MustCompile(`\\\{(.+?)=`).FindStringSubmatch(match)[1] + wildcardNames = append(wildcardNames, name) + return "(.+)" + }) + + // extract and replace {param} patterns + r2 := regexp.MustCompile(`\\\{([^}]+)\\}`) + escaped = r2.ReplaceAllStringFunc(escaped, func(match string) string { + // extract wildcard name from the curl braces {}. + name := regexp.MustCompile(`\\\{(.*?)\\}`).FindStringSubmatch(match)[1] + wildcardNames = append(wildcardNames, name) + return "([^/]+)" + }) + + return "^" + escaped + "$", wildcardNames +} + +// createMessageFromJSON creates a message from the uriMatch given the JSON body in the http request. +func createMessageFromJSON(match *uriMatch, r *http.Request) (gogoproto.Message, error) { + requestType := gogoproto.MessageType(match.QueryInputName) + if requestType == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request type") + } + + msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) + if !ok { + return nil, status.Error(codes.Internal, "failed to cast to proto message") + } + + defer r.Body.Close() + limitedReader := io.LimitReader(r.Body, maxBodySize) + err := jsonpb.Unmarshal(limitedReader, msg) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + return msg, nil +} + +// createMessage creates a message from the given uriMatch. If the match has params, the message will be populated +// with the value of those params. Otherwise, an empty message is returned. +func createMessage(match *uriMatch) (gogoproto.Message, error) { + requestType := gogoproto.MessageType(match.QueryInputName) + if requestType == nil { + return nil, status.Error(codes.InvalidArgument, "unknown request type") + } + + msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) + if !ok { + return nil, status.Error(codes.Internal, "failed to create message instance") + } + + // if the uri match has params, we need to populate the message with the values of those params. + if match.HasParams() { + // convert flat params map to nested structure + nestedParams := make(map[string]any) + for key, value := range match.Params { + parts := strings.Split(key, ".") + current := nestedParams + + // step through nested levels + for i, part := range parts { + if i == len(parts)-1 { + // Last part - set the value + current[part] = value + } else { + // continue nestedness + if _, exists := current[part]; !exists { + current[part] = make(map[string]any) + } + current = current[part].(map[string]any) + } + } + } + + // Configure decoder to handle the nested structure + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: msg, + TagName: "json", // Use json tags as they're simpler + WeaklyTypedInput: true, + }) + if err != nil { + return nil, status.Error(codes.Internal, "failed to create message instance") + } + + if err := decoder.Decode(nestedParams); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + } + return msg, nil +} diff --git a/server/v2/api/grpcgateway/uri_test.go b/server/v2/api/grpcgateway/uri_test.go new file mode 100644 index 000000000000..4bb74c39e40e --- /dev/null +++ b/server/v2/api/grpcgateway/uri_test.go @@ -0,0 +1,264 @@ +package grpcgateway + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "testing" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" +) + +func TestMatchURI(t *testing.T) { + testCases := []struct { + name string + uri string + mapping map[string]string + expected *uriMatch + }{ + { + name: "simple match, no wildcards", + uri: "https://localhost:8080/foo/bar", + mapping: map[string]string{"/foo/bar": "query.Bank"}, + expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{}}, + }, + { + name: "match with query parameters", + uri: "https://localhost:8080/foo/bar?baz=qux", + mapping: map[string]string{"/foo/bar": "query.Bank"}, + expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{"baz": "qux"}}, + }, + { + name: "match with multiple query parameters", + uri: "https://localhost:8080/foo/bar?baz=qux&foo=/msg.type.bank.send", + mapping: map[string]string{"/foo/bar": "query.Bank"}, + expected: &uriMatch{QueryInputName: "query.Bank", Params: map[string]string{"baz": "qux", "foo": "/msg.type.bank.send"}}, + }, + { + name: "wildcard match at the end", + uri: "https://localhost:8080/foo/bar/buzz", + mapping: map[string]string{"/foo/bar/{baz}": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"baz": "buzz"}, + }, + }, + { + name: "wildcard match in the middle", + uri: "https://localhost:8080/foo/buzz/bar", + mapping: map[string]string{"/foo/{baz}/bar": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"baz": "buzz"}, + }, + }, + { + name: "multiple wild cards", + uri: "https://localhost:8080/foo/bar/baz/buzz", + mapping: map[string]string{"/foo/bar/{q1}/{q2}": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"q1": "baz", "q2": "buzz"}, + }, + }, + { + name: "catch-all wildcard", + uri: "https://localhost:8080/foo/bar/ibc/token/stuff", + mapping: map[string]string{"/foo/bar/{ibc_token=**}": "bar"}, + expected: &uriMatch{ + QueryInputName: "bar", + Params: map[string]string{"ibc_token": "ibc/token/stuff"}, + }, + }, + { + name: "no match should return nil", + uri: "https://localhost:8080/foo/bar", + mapping: map[string]string{"/bar/foo": "bar"}, + expected: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + u, err := url.Parse(tc.uri) + require.NoError(t, err) + actual := matchURL(u, tc.mapping) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestURIMatch_HasParams(t *testing.T) { + u := uriMatch{Params: map[string]string{"foo": "bar"}} + require.True(t, u.HasParams()) + + u = uriMatch{} + require.False(t, u.HasParams()) +} + +type Nested struct { + Foo int `protobuf:"varint,1,opt,name=foo,proto3" json:"foo,omitempty"` +} + +type Pagination struct { + Limit int `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Nest *Nested `protobuf:"bytes,2,opt,name=nest,proto3" json:"nest,omitempty"` +} + +const dummyProtoName = "dummy" + +type DummyProto struct { + Foo string `protobuf:"bytes,1,opt,name=foo,proto3" json:"foo,omitempty"` + Bar bool `protobuf:"varint,2,opt,name=bar,proto3" json:"bar,omitempty"` + Baz int `protobuf:"varint,3,opt,name=baz,proto3" json:"baz,omitempty"` + Page *Pagination `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` +} + +func (d DummyProto) Reset() {} + +func (d DummyProto) String() string { return dummyProtoName } + +func (d DummyProto) ProtoMessage() {} + +func TestCreateMessage(t *testing.T) { + gogoproto.RegisterType(&DummyProto{}, dummyProtoName) + + testCases := []struct { + name string + uri uriMatch + expected gogoproto.Message + expErr bool + }{ + { + name: "simple, empty message", + uri: uriMatch{QueryInputName: dummyProtoName}, + expected: &DummyProto{}, + }, + { + name: "message with params", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "true", "baz": "1352"}, + }, + expected: &DummyProto{ + Foo: "blah", + Bar: true, + Baz: 1352, + }, + }, + { + name: "message with nested params", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "true", "baz": "1352", "page.limit": "3"}, + }, + expected: &DummyProto{ + Foo: "blah", + Bar: true, + Baz: 1352, + Page: &Pagination{Limit: 3}, + }, + }, + { + name: "message with multi nested params", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "true", "baz": "1352", "page.limit": "3", "page.nest.foo": "5"}, + }, + expected: &DummyProto{ + Foo: "blah", + Bar: true, + Baz: 1352, + Page: &Pagination{Limit: 3, Nest: &Nested{Foo: 5}}, + }, + }, + { + name: "invalid params should error out", + uri: uriMatch{ + QueryInputName: dummyProtoName, + Params: map[string]string{"foo": "blah", "bar": "235235", "baz": "true"}, + }, + expErr: true, + }, + { + name: "unknown input type", + uri: uriMatch{ + QueryInputName: "foobar", + }, + expErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := createMessage(&tc.uri) + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + } + }) + } +} + +func TestCreateMessageFromJson(t *testing.T) { + gogoproto.RegisterType(&DummyProto{}, dummyProtoName) + testCases := []struct { + name string + uri uriMatch + request func() *http.Request + expected gogoproto.Message + expErr bool + }{ + { + name: "simple, empty message", + uri: uriMatch{QueryInputName: dummyProtoName}, + request: func() *http.Request { + return &http.Request{Body: io.NopCloser(bytes.NewReader([]byte("{}")))} + }, + expected: &DummyProto{}, + }, + { + name: "message with json input", + uri: uriMatch{QueryInputName: dummyProtoName}, + request: func() *http.Request { + d := DummyProto{ + Foo: "hello", + Bar: true, + Baz: 320, + } + bz, err := json.Marshal(d) + require.NoError(t, err) + return &http.Request{Body: io.NopCloser(bytes.NewReader(bz))} + }, + expected: &DummyProto{ + Foo: "hello", + Bar: true, + Baz: 320, + }, + }, + { + name: "message with invalid json", + uri: uriMatch{QueryInputName: dummyProtoName}, + request: func() *http.Request { + return &http.Request{Body: io.NopCloser(bytes.NewReader([]byte(`{"foo":12,dfi3}"`)))} + }, + expErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := createMessageFromJSON(&tc.uri, tc.request()) + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + } + }) + } +} diff --git a/server/v2/go.mod b/server/v2/go.mod index 6816b30ee2c8..83084a10a392 100644 --- a/server/v2/go.mod +++ b/server/v2/go.mod @@ -32,6 +32,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.35.2 ) @@ -109,7 +110,6 @@ require ( golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/simapp/v2/simdv2/cmd/commands.go b/simapp/v2/simdv2/cmd/commands.go index 2a7634ffea98..f9b9e6ebfc09 100644 --- a/simapp/v2/simdv2/cmd/commands.go +++ b/simapp/v2/simdv2/cmd/commands.go @@ -30,7 +30,6 @@ import ( "github.com/cosmos/cosmos-sdk/client/rpc" sdktelemetry "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/version" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" @@ -157,6 +156,7 @@ func InitRootCmd[T transaction.Tx]( logger, deps.GlobalConfig, simApp.InterfaceRegistry(), + simApp.App.AppManager, ) if err != nil { return nil, err @@ -278,10 +278,4 @@ func registerGRPCGatewayRoutes[T transaction.Tx]( cmtservice.RegisterGRPCGatewayRoutes(deps.ClientContext, server.GRPCGatewayRouter) _ = nodeservice.RegisterServiceHandlerClient(context.Background(), server.GRPCGatewayRouter, nodeservice.NewServiceClient(deps.ClientContext)) _ = txtypes.RegisterServiceHandlerClient(context.Background(), server.GRPCGatewayRouter, txtypes.NewServiceClient(deps.ClientContext)) - - for _, mod := range deps.ModuleManager.Modules() { - if gmod, ok := mod.(module.HasGRPCGateway); ok { - gmod.RegisterGRPCGatewayRoutes(deps.ClientContext, server.GRPCGatewayRouter) - } - } } diff --git a/tests/systemtests/distribution_test.go b/tests/systemtests/distribution_test.go index 495fd5807e81..0cc381786d32 100644 --- a/tests/systemtests/distribution_test.go +++ b/tests/systemtests/distribution_test.go @@ -179,20 +179,21 @@ func TestDistrValidatorGRPCQueries(t *testing.T) { // test validator slashes grpc endpoint slashURL := baseurl + `/cosmos/distribution/v1beta1/validators/%s/slashes` - invalidHeightOutput := `{"code":3, "message":"strconv.ParseUint: parsing \"-3\": invalid syntax", "details":[]}` + invalidStartingHeightOutput := `{"code":3, "message":"1 error(s) decoding:\n\n* cannot parse 'starting_height' as uint: strconv.ParseUint: parsing \"-3\": invalid syntax", "details":[]}` + invalidEndingHeightOutput := `{"code":3, "message":"1 error(s) decoding:\n\n* cannot parse 'ending_height' as uint: strconv.ParseUint: parsing \"-3\": invalid syntax", "details":[]}` slashTestCases := []systest.RestTestCase{ { Name: "invalid start height", Url: fmt.Sprintf(slashURL+`?starting_height=%s&ending_height=%s`, valOperAddr, "-3", "3"), ExpCode: http.StatusBadRequest, - ExpOut: invalidHeightOutput, + ExpOut: invalidStartingHeightOutput, }, { Name: "invalid end height", Url: fmt.Sprintf(slashURL+`?starting_height=%s&ending_height=%s`, valOperAddr, "1", "-3"), ExpCode: http.StatusBadRequest, - ExpOut: invalidHeightOutput, + ExpOut: invalidEndingHeightOutput, }, { Name: "valid request get slashes",