Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Bug Fixes

* (runtime) [#26469](https://github.com/cosmos/cosmos-sdk/issues/26469) Accumulate all registered autocli msg/query service names per module instead of keeping only the last, so multi-service modules expose every service.
* (client/tx) [#26759](https://github.com/cosmos/cosmos-sdk/issues/26759) Populate the multisig bit array in simulation txs so `--gas auto` works for multisig senders.
* (blockstm) [#26772](https://github.com/cosmos/cosmos-sdk/pull/26772) Panic with a descriptive error when accessing an unregistered store instead of silently using store index zero.
* (x/genutil) [#26741](https://github.com/cosmos/cosmos-sdk/issues/26741) Preserve vote extension enable height when exporting genesis state.
Expand Down
73 changes: 53 additions & 20 deletions runtime/services/autocli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package services

import (
"context"
"strings"

gogogrpc "github.com/cosmos/gogoproto/grpc"
"github.com/cosmos/gogoproto/proto"
Expand Down Expand Up @@ -48,8 +49,8 @@ func ExtractAutoCLIOptions(appModules map[string]any) map[string]*autocliv1.Modu

cfg := &autocliConfigurator{}

// try to auto-discover options based on the last msg and query
// services registered for the module
// try to auto-discover options based on the msg and query services
// registered for the module
if mod, ok := mod.(module.HasServices); ok {
mod.RegisterServices(cfg)
}
Expand All @@ -66,23 +67,11 @@ func ExtractAutoCLIOptions(appModules map[string]any) map[string]*autocliv1.Modu
panic(cfg.Error())
}

haveServices := false
modOptions := &autocliv1.ModuleOptions{}
if cfg.msgServer.serviceName != "" {
haveServices = true
modOptions.Tx = &autocliv1.ServiceCommandDescriptor{
Service: cfg.msgServer.serviceName,
}
}

if cfg.queryServer.serviceName != "" {
haveServices = true
modOptions.Query = &autocliv1.ServiceCommandDescriptor{
Service: cfg.queryServer.serviceName,
}
}
modOptions.Tx = newServiceCommandDescriptor(cfg.msgServer.serviceNames)
modOptions.Query = newServiceCommandDescriptor(cfg.queryServer.serviceNames)

if haveServices {
if modOptions.Tx != nil || modOptions.Query != nil {
moduleOptions[modName] = modOptions
}
}
Expand Down Expand Up @@ -132,13 +121,57 @@ func (a *autocliConfigurator) RegisterService(sd *grpc.ServiceDesc, ss any) {
}
func (a *autocliConfigurator) Error() error { return nil }

// autocliServiceRegistrar is used to capture the service name for registered services
// autocliServiceRegistrar captures the names of every service registered for a
// module. A module may register more than one msg or query service, so all of
// them are retained instead of only the last one.
type autocliServiceRegistrar struct {
serviceName string
serviceNames []string
}

func (a *autocliServiceRegistrar) RegisterService(sd *grpc.ServiceDesc, _ any) {
a.serviceName = sd.ServiceName
a.serviceNames = append(a.serviceNames, sd.ServiceName)
}

// newServiceCommandDescriptor builds a ServiceCommandDescriptor from the service
// names registered for a single command type (tx or query). It returns nil when
// no service was registered.
//
// The first service is exposed as the primary command. Any additional services
// are attached as sub-commands so they are no longer silently dropped. The
// sub-command name defaults to the lowercased short name of the service, falling
// back to the fully qualified name when that would collide.
func newServiceCommandDescriptor(serviceNames []string) *autocliv1.ServiceCommandDescriptor {
if len(serviceNames) == 0 {
return nil
}

desc := &autocliv1.ServiceCommandDescriptor{Service: serviceNames[0]}
if len(serviceNames) == 1 {
return desc
}

desc.SubCommands = make(map[string]*autocliv1.ServiceCommandDescriptor, len(serviceNames)-1)
for _, name := range serviceNames[1:] {
key := subCommandKey(name, desc.SubCommands)
desc.SubCommands[key] = &autocliv1.ServiceCommandDescriptor{Service: name}
}
return desc
}

// subCommandKey derives a unique sub-command name for a service. It prefers the
// lowercased short name (the segment after the last dot) and falls back to the
// lowercased fully qualified name when the short name is empty or already taken.
func subCommandKey(serviceName string, taken map[string]*autocliv1.ServiceCommandDescriptor) string {
key := serviceName
if i := strings.LastIndexByte(serviceName, '.'); i >= 0 {
key = serviceName[i+1:]
}
key = strings.ToLower(key)

if _, exists := taken[key]; exists || key == "" {
key = strings.ToLower(serviceName)
}
return key
}

var _ autocliv1.QueryServer = &AutoCLIQueryService{}
53 changes: 53 additions & 0 deletions runtime/services/autocli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package services

import (
"testing"

"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

func TestAutoCLIServiceRegistrarAccumulatesServices(t *testing.T) {
var reg autocliServiceRegistrar
reg.RegisterService(&grpc.ServiceDesc{ServiceName: "cosmos.foo.v1.Query"}, nil)
reg.RegisterService(&grpc.ServiceDesc{ServiceName: "cosmos.foo.v2.Query"}, nil)

// Both services must be retained; the second one previously overwrote the first.
require.Equal(t, []string{"cosmos.foo.v1.Query", "cosmos.foo.v2.Query"}, reg.serviceNames)
}

func TestNewServiceCommandDescriptor(t *testing.T) {
require.Nil(t, newServiceCommandDescriptor(nil))

// Single service stays the primary command, no sub-commands.
single := newServiceCommandDescriptor([]string{"cosmos.bank.v1beta1.Query"})
require.Equal(t, "cosmos.bank.v1beta1.Query", single.Service)
require.Empty(t, single.SubCommands)

// Multiple services: first is primary, the rest become sub-commands and are
// no longer dropped.
multi := newServiceCommandDescriptor([]string{
"cosmos.foo.v1.Query",
"cosmos.foo.v1.SecondaryQuery",
})
require.Equal(t, "cosmos.foo.v1.Query", multi.Service)
require.Len(t, multi.SubCommands, 1)
require.Contains(t, multi.SubCommands, "secondaryquery")
require.Equal(t, "cosmos.foo.v1.SecondaryQuery", multi.SubCommands["secondaryquery"].Service)
}

func TestSubCommandKeyCollisionFallsBackToFullName(t *testing.T) {
// Two services share the short name "Query" but differ by version, so the
// second must fall back to its fully qualified name to stay unique.
desc := newServiceCommandDescriptor([]string{
"cosmos.foo.v1.Service",
"cosmos.foo.v1.Query",
"cosmos.foo.v2.Query",
})
require.Equal(t, "cosmos.foo.v1.Service", desc.Service)
require.Len(t, desc.SubCommands, 2)
require.Contains(t, desc.SubCommands, "query")
require.Contains(t, desc.SubCommands, "cosmos.foo.v2.query")
require.Equal(t, "cosmos.foo.v1.Query", desc.SubCommands["query"].Service)
require.Equal(t, "cosmos.foo.v2.Query", desc.SubCommands["cosmos.foo.v2.query"].Service)
}
Loading