feat(server/v2)!: grpcgateway autoregistration (#22941)
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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.36.1
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user