fix(autocli): fix simapp enhancing (#15906)

This commit is contained in:
Julien Robert 2023-04-25 16:08:02 +02:00 committed by GitHub
parent e77156858f
commit ebcad58468
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 309 additions and 1259 deletions

View File

@ -4268,9 +4268,9 @@ type ModuleOptions struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// tx describes the tx command for the module.
// tx describes the tx commands for the module.
Tx *ServiceCommandDescriptor `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"`
// query describes the tx command for the module.
// query describes the queries commands for the module.
Query *ServiceCommandDescriptor `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"`
}

View File

@ -355,10 +355,11 @@ func GetClientContextFromCmd(cmd *cobra.Command) Context {
}
// SetCmdClientContext sets a command's Context value to the provided argument.
// If the context has not been set, set the given context as the default.
func SetCmdClientContext(cmd *cobra.Command, clientCtx Context) error {
v := cmd.Context().Value(ClientContextKey)
if v == nil {
return errors.New("client context not set")
v = &clientCtx
}
clientCtxPtr := v.(*Context)

37
client/v2/CHANGELOG.md Normal file
View File

@ -0,0 +1,37 @@
<!--
Guiding Principles:
Changelogs are for humans, not machines.
There should be an entry for every single version.
The same types of changes should be grouped.
Versions and sections should be linkable.
The latest version comes first.
The release date of each version is displayed.
Mention whether you follow Semantic Versioning.
Usage:
Change log entries are to be added to the Unreleased section under the
appropriate stanza (see below). Each entry should ideally include a tag and
the Github issue reference in the following format:
* (<tag>) \#<issue-number> message
The issue numbers will later be link-ified during the release process so you do
not have to worry about including a link manually, but you can if you wish.
Types of changes (Stanzas):
"Features" for new features.
"Improvements" for changes in existing functionality.
"Deprecated" for soon-to-be removed features.
"Bug Fixes" for any bug fixes.
"Client Breaking" for breaking Protobuf, gRPC and REST routes used by end-users.
"CLI Breaking" for breaking CLI commands.
"API Breaking" for breaking exported APIs used by developers building on SDK.
Ref: https://keepachangelog.com/en/1.0.0/
-->
# Changelog
## [Unreleased]

7
client/v2/README.md Normal file
View File

@ -0,0 +1,7 @@
# AutoCLI
The `autocli` package is a Go library for generating CLIs (command line interfaces) for Cosmos SDK-based applications.
Read more about in it the Cosmos SDK documentation:
* https://docs.cosmos.network/main/building-modules/autocli

View File

@ -1,9 +1,9 @@
package autocli
import (
"fmt"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"cosmossdk.io/client/v2/autocli/flag"
"cosmossdk.io/core/address"
"cosmossdk.io/core/appmodule"
"cosmossdk.io/depinject"
@ -32,23 +32,18 @@ type AppOptions struct {
// app to override module options if they are either not provided by a
// module or need to be improved.
ModuleOptions map[string]*autocliv1.ModuleOptions `optional:"true"`
}
// RootCmd generates a root command for an app based on the AppOptions. This
// command currently only includes query commands but will be enhanced over
// time to cover the full scope of an app CLI.
func (appOptions AppOptions) RootCmd() (*cobra.Command, error) {
rootCmd := &cobra.Command{}
err := appOptions.EnhanceRootCommand(rootCmd)
return rootCmd, err
// AddressCodec is the address codec to use for the app.
// If not provided the default address prefix will be fetched from the reflection client.
AddressCodec address.Codec `optional:"true"`
}
// EnhanceRootCommand enhances the provided root command with autocli AppOptions,
// only adding missing query commands and doesn't override commands already
// only adding missing commands and doesn't override commands already
// in the root command. This allows for the graceful integration of autocli with
// existing app CLI commands where autocli simply automatically adds things that
// weren't manually provided. It does take into account custom query commands
// provided by modules with the HasCustomQueryCommand extension interface.
// weren't manually provided. It does take into account custom commands
// provided by modules with the HasCustomQueryCommand or HasCustomTxCommand extension interface.
// Example Usage:
//
// var autoCliOpts autocli.AppOptions
@ -60,6 +55,12 @@ func (appOptions AppOptions) RootCmd() (*cobra.Command, error) {
// err = autoCliOpts.EnhanceRootCommand(rootCmd)
func (appOptions AppOptions) EnhanceRootCommand(rootCmd *cobra.Command) error {
builder := &Builder{
Builder: flag.Builder{
AddressCodec: appOptions.AddressCodec,
GetClientConn: func() (grpc.ClientConnInterface, error) {
return client.GetClientQueryContext(rootCmd)
},
},
GetClientConn: func(cmd *cobra.Command) (grpc.ClientConnInterface, error) {
return client.GetClientQueryContext(cmd)
},
@ -71,19 +72,8 @@ func (appOptions AppOptions) EnhanceRootCommand(rootCmd *cobra.Command) error {
}
func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Command, builder *Builder) error {
moduleOptions := appOptions.ModuleOptions
if moduleOptions == nil {
moduleOptions = map[string]*autocliv1.ModuleOptions{}
for name, module := range appOptions.Modules {
if module, ok := module.(HasAutoCLIConfig); ok {
moduleOptions[name] = module.AutoCLIOptions()
}
}
}
customQueryCmds := map[string]*cobra.Command{}
customMsgCmds := map[string]*cobra.Command{}
// extract any custom commands from modules
customQueryCmds, customMsgCmds := map[string]*cobra.Command{}, map[string]*cobra.Command{}
for name, module := range appOptions.Modules {
if queryModule, ok := module.(HasCustomQueryCommand); ok {
queryCmd := queryModule.GetQueryCmd()
@ -101,27 +91,12 @@ func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Comman
}
}
// if we have an existing query command, enhance it or build a custom one
enhanceQuery := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
queryCmdDesc := modOpts.Query
if queryCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
err := builder.AddQueryServiceCommands(cmd, queryCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if queryCmd := findSubCommand(rootCmd, "query"); queryCmd != nil {
if err := builder.enhanceCommandCommon(queryCmd, moduleOptions, customQueryCmds, enhanceQuery); err != nil {
if err := builder.enhanceCommandCommon(queryCmd, appOptions, customQueryCmds, enhanceQuery); err != nil {
return err
}
} else {
queryCmd, err := builder.BuildQueryCommand(moduleOptions, customQueryCmds)
queryCmd, err := builder.BuildQueryCommand(appOptions, customQueryCmds, enhanceQuery)
if err != nil {
return err
}
@ -129,26 +104,12 @@ func (appOptions AppOptions) EnhanceRootCommandWithBuilder(rootCmd *cobra.Comman
rootCmd.AddCommand(queryCmd)
}
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transactions commands for the %s module", moduleName))
err := builder.AddQueryServiceCommands(cmd, txCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if msgCmd := findSubCommand(rootCmd, "tx"); msgCmd != nil {
if err := builder.enhanceCommandCommon(msgCmd, moduleOptions, customQueryCmds, enhanceMsg); err != nil {
if err := builder.enhanceCommandCommon(msgCmd, appOptions, customMsgCmds, enhanceMsg); err != nil {
return err
}
} else {
subCmd, err := builder.BuildMsgCommand(moduleOptions, customQueryCmds)
subCmd, err := builder.BuildMsgCommand(appOptions, customMsgCmds, enhanceMsg)
if err != nil {
return err
}

View File

@ -7,6 +7,7 @@ import (
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
"google.golang.org/protobuf/reflect/protoreflect"
"sigs.k8s.io/yaml"
@ -67,22 +68,27 @@ func (b *Builder) buildMethodCommandCommon(descriptor protoreflect.MethodDescrip
// options or the provided custom commands for each module. If the provided query command already contains a command
// for a module, that command is not over-written by this method. This allows a graceful addition of autocli to
// automatically fill in missing commands.
func (b *Builder) enhanceCommandCommon(cmd *cobra.Command, moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command, buildModuleCommand func(*cobra.Command, *autocliv1.ModuleOptions, string) error) error {
allModuleNames := map[string]bool{}
for moduleName := range moduleOptions {
allModuleNames[moduleName] = true
}
for moduleName := range customCmds {
allModuleNames[moduleName] = true
func (b *Builder) enhanceCommandCommon(
cmd *cobra.Command,
appOptions AppOptions,
customCmds map[string]*cobra.Command,
buildModuleCommand enhanceCommandFunc,
) error {
moduleOptions := appOptions.ModuleOptions
if len(moduleOptions) == 0 {
moduleOptions = map[string]*autocliv1.ModuleOptions{}
for name, module := range appOptions.Modules {
if module, ok := module.(HasAutoCLIConfig); ok {
moduleOptions[name] = module.AutoCLIOptions()
}
}
}
for moduleName := range allModuleNames {
modules := append(maps.Keys(appOptions.Modules), maps.Keys(moduleOptions)...)
for _, moduleName := range modules {
// if we have an existing command skip adding one here
if cmd.HasSubCommands() {
if _, _, err := cmd.Find([]string{moduleName}); err == nil {
// command already exists, skip
continue
}
if findSubCommand(cmd, moduleName) != nil {
continue
}
// if we have a custom command use that instead of generating one
@ -98,8 +104,7 @@ func (b *Builder) enhanceCommandCommon(cmd *cobra.Command, moduleOptions map[str
continue
}
err := buildModuleCommand(cmd, modOpts, moduleName)
if err != nil {
if err := buildModuleCommand(b, moduleName, cmd, modOpts); err != nil {
return err
}
}
@ -107,19 +112,51 @@ func (b *Builder) enhanceCommandCommon(cmd *cobra.Command, moduleOptions map[str
return nil
}
type enhanceCommandFunc func(builder *Builder, moduleName string, cmd *cobra.Command, modOpts *autocliv1.ModuleOptions) error
// enhanceQuery enhances the provided query command with the autocli commands for a module.
func enhanceQuery(builder *Builder, moduleName string, cmd *cobra.Command, modOpts *autocliv1.ModuleOptions) error {
queryCmdDesc := modOpts.Query
if queryCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
if err := builder.AddQueryServiceCommands(subCmd, queryCmdDesc); err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
// enhanceMsg enhances the provided msg command with the autocli commands for a module.
func enhanceMsg(builder *Builder, moduleName string, cmd *cobra.Command, modOpts *autocliv1.ModuleOptions) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transactions commands for the %s module", moduleName))
if err := builder.AddMsgServiceCommands(subCmd, txCmdDesc); err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
// outOrStdoutFormat formats the output based on the output flag and writes it to the command's output stream.
func (b *Builder) outOrStdoutFormat(cmd *cobra.Command, out []byte) error {
var err error
outputType := cmd.Flag(flags.FlagOutput)
// if the output type is text, convert the json to yaml
// if output type is json or nil, default to json
if outputType != nil && outputType.Value.String() == "text" {
if outputType != nil && outputType.Value.String() == flags.OutputFormatText {
out, err = yaml.JSONToYAML(out)
if err != nil {
return err
}
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(out))
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(out))
return err
}

View File

@ -2,16 +2,19 @@ package flag
import (
"context"
"fmt"
reflectionv2alpha1 "cosmossdk.io/api/cosmos/base/reflection/v2alpha1"
"github.com/cosmos/cosmos-sdk/types"
"cosmossdk.io/core/address"
"google.golang.org/protobuf/reflect/protoreflect"
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
)
type addressStringType struct{}
func (a addressStringType) NewValue(ctx context.Context, b *Builder) Value {
if b.AddressPrefix == "" {
if b.AddressCodec == nil {
conn, err := b.GetClientConn()
if err != nil {
panic(err)
@ -24,9 +27,11 @@ func (a addressStringType) NewValue(ctx context.Context, b *Builder) Value {
if resp == nil || resp.Config == nil {
panic("bech32 account address prefix is not set")
}
b.AddressPrefix = resp.Config.Bech32AccountAddressPrefix
b.AddressCodec = addresscodec.NewBech32Codec(resp.Config.Bech32AccountAddressPrefix)
}
return &addressValue{addressPrefix: b.AddressPrefix}
return &addressValue{addressCodec: b.AddressCodec}
}
func (a addressStringType) DefaultValue() string {
@ -34,8 +39,8 @@ func (a addressStringType) DefaultValue() string {
}
type addressValue struct {
value string
addressPrefix string
value string
addressCodec address.Codec
}
func (a addressValue) Get(protoreflect.Value) (protoreflect.Value, error) {
@ -48,10 +53,11 @@ func (a addressValue) String() string {
// Set implements the flag.Value interface for addressValue it only supports bech32 addresses.
func (a *addressValue) Set(s string) error {
_, err := types.GetFromBech32(s, a.addressPrefix)
_, err := a.addressCodec.StringToBytes(s)
if err != nil {
return err
return fmt.Errorf("invalid bech32 account address: %w", err)
}
a.value = s
return nil

View File

@ -5,6 +5,8 @@ import (
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"cosmossdk.io/core/address"
)
// Builder manages options for building pflag flags for protobuf messages.
@ -23,9 +25,10 @@ type Builder struct {
messageFlagTypes map[protoreflect.FullName]Type
scalarFlagTypes map[string]Type
// AddressPrefix is the prefix for the address flag
AddressPrefix string
// reflectionClient is the reflection client for the address flag
// AddressCodec is the address codec used for the address flag
AddressCodec address.Codec
// GetClientConn is the reflection client for the address flag
GetClientConn func() (grpc.ClientConnInterface, error)
}

View File

@ -14,22 +14,9 @@ import (
// BuildMsgCommand builds the msg commands for all the provided modules. If a custom command is provided for a
// module, this is used instead of any automatically generated CLI commands. This allows apps to a fully dynamic client
// with a more customized experience if a binary with custom commands is downloaded.
func (b *Builder) BuildMsgCommand(moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command) (*cobra.Command, error) {
func (b *Builder) BuildMsgCommand(appOptions AppOptions, customCmds map[string]*cobra.Command, buildModuleCommand enhanceCommandFunc) (*cobra.Command, error) {
msgCmd := topLevelCmd("tx", "Transaction subcommands")
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transactions commands for the %s module", moduleName))
err := b.AddMsgServiceCommands(subCmd, txCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if err := b.enhanceCommandCommon(msgCmd, moduleOptions, customCmds, enhanceMsg); err != nil {
if err := b.enhanceCommandCommon(msgCmd, appOptions, customCmds, enhanceMsg); err != nil {
return nil, err
}
@ -78,8 +65,7 @@ func (b *Builder) AddMsgServiceCommands(cmd *cobra.Command, cmdDescriptor *autoc
}
methodsLength := methods.Len()
for i := 0; i < methodsLength; i++ {
for i := 0; i < methods.Len(); i++ {
methodDescriptor := methods.Get(i)
methodOpts, ok := rpcOptMap[methodDescriptor.Name()]
if !ok {
@ -89,10 +75,12 @@ func (b *Builder) AddMsgServiceCommands(cmd *cobra.Command, cmdDescriptor *autoc
if methodOpts.Skip {
continue
}
methodCmd, err := b.BuildMsgMethodCommand(methodDescriptor, methodOpts)
if err != nil {
return err
}
if methodCmd != nil {
cmd.AddCommand(methodCmd)
}
@ -101,6 +89,7 @@ func (b *Builder) AddMsgServiceCommands(cmd *cobra.Command, cmdDescriptor *autoc
return nil
}
// BuildMsgMethodCommand returns a command that outputs the JSON representation of the message.
func (b *Builder) BuildMsgMethodCommand(descriptor protoreflect.MethodDescriptor, options *autocliv1.RpcCommandOptions) (*cobra.Command, error) {
jsonMarshalOptions := protojson.MarshalOptions{
Indent: " ",
@ -116,11 +105,12 @@ func (b *Builder) BuildMsgMethodCommand(descriptor protoreflect.MethodDescriptor
return err
}
err = b.outOrStdoutFormat(cmd, bz)
return err
return b.outOrStdoutFormat(cmd, bz)
})
if b.AddTxConnFlags != nil {
b.AddTxConnFlags(cmd)
}
return cmd, err
}

View File

@ -5,14 +5,12 @@ import (
"strings"
"testing"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
"cosmossdk.io/client/v2/internal/testpb"
)
@ -225,18 +223,22 @@ func TestHelpMsg(t *testing.T) {
golden.Assert(t, conn.out.String(), "help-deprecated-msg.golden")
}
func TestBuildCustomMsgCommand(t *testing.T) {
func TestBuildMsgCommand(t *testing.T) {
b := &Builder{}
customCommandCalled := false
cmd, err := b.BuildMsgCommand(map[string]*autocliv1.ModuleOptions{
"test": {
Tx: testCmdMsgDesc,
appOptions := AppOptions{
ModuleOptions: map[string]*autocliv1.ModuleOptions{
"test": {
Tx: testCmdMsgDesc,
},
},
}, map[string]*cobra.Command{
}
cmd, err := b.BuildMsgCommand(appOptions, map[string]*cobra.Command{
"test": {Use: "test", Run: func(cmd *cobra.Command, args []string) {
customCommandCalled = true
}},
})
}, enhanceMsg)
assert.NilError(t, err)
cmd.SetArgs([]string{"test", "tx"})
assert.NilError(t, cmd.Execute())
@ -260,21 +262,20 @@ func TestErrorBuildMsgCommand(t *testing.T) {
},
}
opts := map[string]*autocliv1.ModuleOptions{
"test": {
Tx: commandDescriptor,
appOptions := AppOptions{
ModuleOptions: map[string]*autocliv1.ModuleOptions{
"test": {
Tx: commandDescriptor,
},
},
}
_, err := b.BuildMsgCommand(opts, nil)
_, err := b.BuildMsgCommand(appOptions, nil, enhanceMsg)
assert.ErrorContains(t, err, "can't find field un-existent-proto-field")
nonExistentService := &autocliv1.ServiceCommandDescriptor{Service: "un-existent-service"}
opts = map[string]*autocliv1.ModuleOptions{
"test": {
Tx: nonExistentService,
},
}
_, err = b.BuildMsgCommand(opts, nil)
appOptions.ModuleOptions["test"].Tx = nonExistentService
_, err = b.BuildMsgCommand(appOptions, nil, enhanceMsg)
assert.ErrorContains(t, err, "can't find service un-existent-service")
}
@ -328,42 +329,35 @@ func TestNotFoundErrorsMsg(t *testing.T) {
func TestEnhanceMessageCommand(t *testing.T) {
b := &Builder{}
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txCmdDesc := modOpts.Tx
if txCmdDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Transactions commands for the %s module", moduleName))
err := b.AddMsgServiceCommands(cmd, txCmdDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
// Test that the command has a subcommand
cmd := &cobra.Command{Use: "test"}
cmd.AddCommand(&cobra.Command{Use: "test"})
options := map[string]*autocliv1.ModuleOptions{
"test": {},
appOptions := AppOptions{
ModuleOptions: map[string]*autocliv1.ModuleOptions{
"test": {},
},
}
err := b.enhanceCommandCommon(cmd, options, map[string]*cobra.Command{}, enhanceMsg)
err := b.enhanceCommandCommon(cmd, appOptions, map[string]*cobra.Command{}, enhanceMsg)
assert.NilError(t, err)
cmd = &cobra.Command{Use: "test"}
options = map[string]*autocliv1.ModuleOptions{}
appOptions.ModuleOptions = map[string]*autocliv1.ModuleOptions{}
customCommands := map[string]*cobra.Command{
"test2": {Use: "test"},
}
err = b.enhanceCommandCommon(cmd, options, customCommands, enhanceMsg)
err = b.enhanceCommandCommon(cmd, appOptions, customCommands, enhanceMsg)
assert.NilError(t, err)
cmd = &cobra.Command{Use: "test"}
options = map[string]*autocliv1.ModuleOptions{
"test": {Tx: nil},
appOptions = AppOptions{
ModuleOptions: map[string]*autocliv1.ModuleOptions{
"test": {Tx: nil},
},
}
customCommands = map[string]*cobra.Command{}
err = b.enhanceCommandCommon(cmd, options, customCommands, enhanceMsg)
err = b.enhanceCommandCommon(cmd, appOptions, customCommands, enhanceMsg)
assert.NilError(t, err)
}

View File

@ -16,24 +16,11 @@ import (
// BuildQueryCommand builds the query commands for all the provided modules. If a custom command is provided for a
// module, this is used instead of any automatically generated CLI commands. This allows apps to a fully dynamic client
// with a more customized experience if a binary with custom commands is downloaded.
func (b *Builder) BuildQueryCommand(moduleOptions map[string]*autocliv1.ModuleOptions, customCmds map[string]*cobra.Command) (*cobra.Command, error) {
func (b *Builder) BuildQueryCommand(appOptions AppOptions, customCmds map[string]*cobra.Command, enhanceQuery enhanceCommandFunc) (*cobra.Command, error) {
queryCmd := topLevelCmd("query", "Querying subcommands")
queryCmd.Aliases = []string{"q"}
enhanceMsg := func(cmd *cobra.Command, modOpts *autocliv1.ModuleOptions, moduleName string) error {
txQueryDesc := modOpts.Query
if txQueryDesc != nil {
subCmd := topLevelCmd(moduleName, fmt.Sprintf("Querying commands for the %s module", moduleName))
err := b.AddQueryServiceCommands(subCmd, txQueryDesc)
if err != nil {
return err
}
cmd.AddCommand(subCmd)
}
return nil
}
if err := b.enhanceCommandCommon(queryCmd, moduleOptions, customCmds, enhanceMsg); err != nil {
if err := b.enhanceCommandCommon(queryCmd, appOptions, customCmds, enhanceQuery); err != nil {
return nil, err
}
@ -99,7 +86,6 @@ func (b *Builder) AddQueryServiceCommands(cmd *cobra.Command, cmdDescriptor *aut
}
cmd.AddCommand(methodCmd)
}
return nil

View File

@ -336,15 +336,20 @@ func TestDeprecated(t *testing.T) {
func TestBuildCustomQueryCommand(t *testing.T) {
b := &Builder{}
customCommandCalled := false
cmd, err := b.BuildQueryCommand(map[string]*autocliv1.ModuleOptions{
"test": {
Query: testCmdDesc,
appOptions := AppOptions{
ModuleOptions: map[string]*autocliv1.ModuleOptions{
"test": {
Query: testCmdDesc,
},
},
}, map[string]*cobra.Command{
}
cmd, err := b.BuildQueryCommand(appOptions, map[string]*cobra.Command{
"test": {Use: "test", Run: func(cmd *cobra.Command, args []string) {
customCommandCalled = true
}},
})
}, enhanceQuery)
assert.NilError(t, err)
cmd.SetArgs([]string{"test", "query"})
assert.NilError(t, cmd.Execute())

View File

@ -11,6 +11,7 @@ require (
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230424095137-b73c17cb9cc8
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
google.golang.org/grpc v1.54.0
google.golang.org/protobuf v1.30.0
gotest.tools/v3 v3.4.0
@ -104,7 +105,6 @@ require (
github.com/zondax/hid v0.9.1 // indirect
github.com/zondax/ledger-go v0.14.1 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect

View File

@ -24,7 +24,7 @@ The `autocli` package is a [Go library](https://pkg.go.dev/cosmossdk.io/client/v
Here are the steps to use the `autocli` package:
1. Define your app's modules that implement the `appmodule.AppModule` interface.
2. (optional) When willing to configure how behave `autocli` command generation, implement the `func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions` method on the module. Learn more [here](#advanced-usage).
2. Configure how behave `autocli` command generation, by implementing the `func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions` method on the module. Learn more [here](#advanced-usage).
3. Use the `autocli.AppOptions` struct to specifies the modules you defined. If you are using the `depinject` package to manage your app's dependencies, it can automatically create an instance of `autocli.AppOptions` based on your app's configuration.
4. Use the `EnhanceRootCommand()` method provided by `autocli` to add the CLI commands for the specified modules to your root command and can also be found in the `client/v2/autocli/app.go` file. Additionally, this method adds the `autocli` functionality to your app's root command. This method is additive only, meaning that it does not create commands if they are already registered for a module. Instead, it adds any missing commands to the root command.

View File

@ -53,13 +53,13 @@ x/{module_name}
├── module
│   └── module.go
│   └── abci.go
│   └── autocli.go
├── simulation
│   ├── decoder.go
│   ├── genesis.go
│   ├── operations.go
│   └── params.go
├── {module_name}.pb.go
├── autocli.go
├── codec.go
├── errors.go
├── events.go
@ -80,10 +80,10 @@ x/{module_name}
* `keeper/`: The module's `Keeper` and `MsgServer` implementation.
* `module/`: The module's `AppModule` and `AppModuleBasic` implementation.
* `abci.go`: The module's `BeginBlocker` and `EndBlocker` implementations (this file is only required if `BeginBlocker` and/or `EndBlocker` need to be defined).
* `autocli.go`: The module [autocli](./10-autocli.md) options.
* `simulation/`: The module's [simulation](./14-simulator.md) package defines functions used by the blockchain simulator application (`simapp`).
* `REAMDE.md`: The module's specification documents outlining important concepts, state storage structure, and message and event type definitions. Learn more how to write module specs in the [spec guidelines](../spec/SPEC-SPEC.md).
* The root directory includes type definitions for messages, events, and genesis state, including the type definitions generated by Protocol Buffers.
* `autocli.go`: The module [autocli](./10-autocli.md) options.
* `codec.go`: The module's registry methods for interface types.
* `errors.go`: The module's sentinel errors.
* `events.go`: The module's event types and constructors.

View File

@ -6,10 +6,10 @@ option go_package = "cosmossdk.io/api/cosmos/base/cli/v1;cliv1";
// ModuleOptions describes the CLI options for a Cosmos SDK module.
message ModuleOptions {
// tx describes the tx command for the module.
// tx describes the tx commands for the module.
ServiceCommandDescriptor tx = 1;
// query describes the tx command for the module.
// query describes the queries commands for the module.
ServiceCommandDescriptor query = 2;
}

View File

@ -213,10 +213,11 @@ func GetServerContextFromCmd(cmd *cobra.Command) *Context {
}
// SetCmdServerContext sets a command's Context value to the provided argument.
// If the context has not been set, set the given context as the default.
func SetCmdServerContext(cmd *cobra.Command, serverCtx *Context) error {
v := cmd.Context().Value(ServerContextKey)
if v == nil {
return errors.New("server context not set")
v = serverCtx
}
serverCtxPtr := v.(*Context)

View File

@ -5,7 +5,7 @@ go 1.20
require (
cosmossdk.io/api v0.4.1
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba
cosmossdk.io/core v0.6.1
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4
cosmossdk.io/depinject v1.0.0-alpha.3
cosmossdk.io/log v1.0.0
cosmossdk.io/math v1.0.0
@ -197,6 +197,7 @@ require (
replace (
// TODO tag all extracted modules after SDK refactor
cosmossdk.io/api => ../api
cosmossdk.io/client/v2 => ../client/v2
cosmossdk.io/store => ../store
cosmossdk.io/tools/confix => ../tools/confix
cosmossdk.io/tools/rosetta => ../tools/rosetta

View File

@ -188,12 +188,10 @@ cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xX
cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba h1:LuPHCncU2KLMNPItFECs709uo46I9wSu2fAWYVCx+/U=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba/go.mod h1:SXdwqO7cN5htalh/lhXWP8V4zKtBrhhcSTU+ytuEtmM=
cosmossdk.io/collections v0.1.0 h1:nzJGeiq32KnZroSrhB6rPifw4I85Cgmzw/YAmr4luv8=
cosmossdk.io/collections v0.1.0/go.mod h1:xbauc0YsbUF8qKMVeBZl0pFCunxBIhKN/WlxpZ3lBuo=
cosmossdk.io/core v0.6.1 h1:OBy7TI2W+/gyn2z40vVvruK3di+cAluinA6cybFbE7s=
cosmossdk.io/core v0.6.1/go.mod h1:g3MMBCBXtxbDWBURDVnJE7XML4BG5qENhs0gzkcpuFA=
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4 h1:l1scDTT2VX18ZuR6P0irvT/bAP0h4297D/Lka5nz2vE=
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4/go.mod h1:J8R0E7soOpQFVqFiFd7EKepXCPpINa2n2t2EqbEsXnY=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=
cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU=
cosmossdk.io/errors v1.0.0-beta.7 h1:gypHW76pTQGVnHKo6QBkb4yFOJjC+sUGRc5Al3Odj1w=

View File

@ -239,8 +239,6 @@ func queryCommand() *cobra.Command {
authcmd.QueryTxCmd(),
)
simapp.ModuleBasics.AddQueryCommands(cmd)
return cmd
}
@ -265,8 +263,6 @@ func txCommand() *cobra.Command {
authcmd.GetAuxToFeeCommand(),
)
simapp.ModuleBasics.AddTxCommands(cmd)
return cmd
}

View File

@ -1,399 +0,0 @@
package nft
import (
"cosmossdk.io/x/nft"
)
func (s *E2ETestSuite) TestQueryClass() {
val := s.network.Validators[0]
testCases := []struct {
name string
args struct {
ClassID string
}
expectErr bool
}{
{
name: "class id does not exist",
args: struct {
ClassID string
}{
ClassID: "class",
},
expectErr: true,
},
{
name: "class id exist",
args: struct {
ClassID string
}{
ClassID: testClassID,
},
expectErr: false,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQueryClass(val, tc.args.ClassID)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
var result nft.QueryClassResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().EqualValues(ExpClass, *result.Class)
}
})
}
}
func (s *E2ETestSuite) TestQueryClasses() {
val := s.network.Validators[0]
testCases := []struct {
name string
expectErr bool
}{
{
name: "no params",
expectErr: false,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQueryClasses(val)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
var result nft.QueryClassesResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().Len(result.Classes, 1)
s.Require().EqualValues(ExpClass, *result.Classes[0])
}
})
}
}
func (s *E2ETestSuite) TestQueryNFT() {
val := s.network.Validators[0]
testCases := []struct {
name string
args struct {
ClassID string
ID string
}
expectErr bool
}{
{
name: "class id does not exist",
args: struct {
ClassID string
ID string
}{
ClassID: "class",
ID: testID,
},
expectErr: true,
},
{
name: "nft id does not exist",
args: struct {
ClassID string
ID string
}{
ClassID: testClassID,
ID: "id",
},
expectErr: true,
},
{
name: "exist nft",
args: struct {
ClassID string
ID string
}{
ClassID: testClassID,
ID: testID,
},
expectErr: false,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQueryNFT(val, tc.args.ClassID, tc.args.ID)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
var result nft.QueryNFTResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().EqualValues(ExpNFT, *result.Nft)
}
})
}
}
func (s *E2ETestSuite) TestQueryNFTs() {
val := s.network.Validators[0]
testCases := []struct {
name string
args struct {
ClassID string
Owner string
}
expectErr bool
expectResult []*nft.NFT
}{
{
name: "class id does not exist",
args: struct {
ClassID string
Owner string
}{
ClassID: "class",
Owner: val.Address.String(),
},
expectErr: false,
expectResult: []*nft.NFT{},
},
{
name: "owner does not exist",
args: struct {
ClassID string
Owner string
}{
ClassID: testClassID,
Owner: s.owner.String(),
},
expectErr: false,
expectResult: []*nft.NFT{},
},
{
name: "class id and owner both does not exist",
args: struct {
ClassID string
Owner string
}{},
expectErr: true,
expectResult: []*nft.NFT{},
},
{
name: "nft exist",
args: struct {
ClassID string
Owner string
}{
ClassID: testClassID,
Owner: val.Address.String(),
},
expectErr: false,
expectResult: []*nft.NFT{&ExpNFT},
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQueryNFTs(val, tc.args.ClassID, tc.args.Owner)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
var result nft.QueryNFTsResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().EqualValues(tc.expectResult, result.Nfts)
}
})
}
}
func (s *E2ETestSuite) TestQueryOwner() {
val := s.network.Validators[0]
testCases := []struct {
name string
args struct {
ClassID string
ID string
}
expectErr bool
errorMsg string
expectResult string
}{
{
name: "class id does not exist",
args: struct {
ClassID string
ID string
}{
ClassID: "class",
ID: testID,
},
expectErr: false,
expectResult: "",
},
{
name: "nft id does not exist",
args: struct {
ClassID string
ID string
}{
ClassID: testClassID,
ID: "nft-id",
},
expectErr: false,
expectResult: "",
},
{
name: "nft exist",
args: struct {
ClassID string
ID string
}{
ClassID: testClassID,
ID: testID,
},
expectErr: false,
expectResult: val.Address.String(),
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQueryOwner(val, tc.args.ClassID, tc.args.ID)
if tc.expectErr {
s.Require().Contains(string(resp.Bytes()), tc.errorMsg)
} else {
s.Require().NoError(err)
var result nft.QueryOwnerResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().EqualValues(tc.expectResult, result.Owner)
}
})
}
}
func (s *E2ETestSuite) TestQueryBalance() {
val := s.network.Validators[0]
testCases := []struct {
name string
args struct {
ClassID string
Owner string
}
expectErr bool
errorMsg string
expectResult uint64
}{
{
name: "class id does not exist",
args: struct {
ClassID string
Owner string
}{
ClassID: "class",
Owner: val.Address.String(),
},
expectErr: false,
expectResult: 0,
},
{
name: "owner does not exist",
args: struct {
ClassID string
Owner string
}{
ClassID: testClassID,
Owner: s.owner.String(),
},
expectErr: false,
expectResult: 0,
},
{
name: "nft exist",
args: struct {
ClassID string
Owner string
}{
ClassID: testClassID,
Owner: val.Address.String(),
},
expectErr: false,
expectResult: 1,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQueryBalance(val, tc.args.ClassID, tc.args.Owner)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
var result nft.QueryBalanceResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().EqualValues(tc.expectResult, result.Amount)
}
})
}
}
func (s *E2ETestSuite) TestQuerySupply() {
val := s.network.Validators[0]
testCases := []struct {
name string
args struct {
ClassID string
}
expectErr bool
errorMsg string
expectResult uint64
}{
{
name: "class id is empty",
args: struct {
ClassID string
}{
ClassID: "",
},
expectErr: true,
errorMsg: nft.ErrEmptyClassID.Error(),
expectResult: 0,
},
{
name: "class id does not exist",
args: struct {
ClassID string
}{
ClassID: "class",
},
expectErr: false,
expectResult: 0,
},
{
name: "class id exist",
args: struct {
ClassID string
}{
ClassID: testClassID,
},
expectErr: false,
expectResult: 1,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
resp, err := ExecQuerySupply(val, tc.args.ClassID)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
var result nft.QuerySupplyResponse
err = val.ClientCtx.Codec.UnmarshalJSON(resp.Bytes(), &result)
s.Require().NoError(err)
s.Require().EqualValues(tc.expectResult, result.Amount)
}
})
}
}

View File

@ -1,76 +0,0 @@
package nft
import (
"fmt"
"cosmossdk.io/x/nft/client/cli"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec/address"
"github.com/cosmos/cosmos-sdk/testutil"
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
"github.com/cosmos/cosmos-sdk/testutil/network"
)
func ExecSend(val *network.Validator, args []string) (testutil.BufferWriter, error) {
cmd := cli.NewCmdSend()
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQueryClass(val *network.Validator, classID string) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQueryClass()
var args []string
args = append(args, classID)
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQueryClasses(val *network.Validator) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQueryClasses()
var args []string
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQueryNFT(val *network.Validator, classID, nftID string) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQueryNFT()
var args []string
args = append(args, classID)
args = append(args, nftID)
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQueryNFTs(val *network.Validator, classID, owner string) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQueryNFTs(address.NewBech32Codec("cosmos"))
var args []string
args = append(args, fmt.Sprintf("--%s=%s", cli.FlagClassID, classID))
args = append(args, fmt.Sprintf("--%s=%s", cli.FlagOwner, owner))
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQueryOwner(val *network.Validator, classID, nftID string) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQueryOwner()
var args []string
args = append(args, classID)
args = append(args, nftID)
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQueryBalance(val *network.Validator, classID, owner string) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQueryBalance()
var args []string
args = append(args, owner)
args = append(args, classID)
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}
func ExecQuerySupply(val *network.Validator, classID string) (testutil.BufferWriter, error) {
cmd := cli.GetCmdQuerySupply()
var args []string
args = append(args, classID)
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
return clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/suite"
"cosmossdk.io/x/nft"
"cosmossdk.io/x/nft/client/cli"
"github.com/cosmos/cosmos-sdk/client/flags"
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
"github.com/cosmos/cosmos-sdk/testutil/network"
@ -92,6 +93,7 @@ func (s *E2ETestSuite) TearDownSuite() {
}
func (s *E2ETestSuite) TestCLITxSend() {
cmd := cli.NewCmdSend()
val := s.network.Validators[0]
args := []string{
fmt.Sprintf("--%s=%s", flags.FlagFrom, OwnerName),
@ -122,10 +124,8 @@ func (s *E2ETestSuite) TestCLITxSend() {
s.Run(tc.name, func() {
clientCtx := val.ClientCtx
args = append(args, tc.args...)
out, err := ExecSend(
val,
args,
)
out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args)
if tc.expectErr {
s.Require().Error(err)
} else {

View File

@ -38,7 +38,7 @@ require (
cloud.google.com/go/storage v1.30.0 // indirect
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba // indirect
cosmossdk.io/collections v0.1.0 // indirect
cosmossdk.io/core v0.6.1 // indirect
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.1 // indirect

View File

@ -192,8 +192,8 @@ cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba h1:LuPHCncU2KLMNPItFEC
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba/go.mod h1:SXdwqO7cN5htalh/lhXWP8V4zKtBrhhcSTU+ytuEtmM=
cosmossdk.io/collections v0.1.0 h1:nzJGeiq32KnZroSrhB6rPifw4I85Cgmzw/YAmr4luv8=
cosmossdk.io/collections v0.1.0/go.mod h1:xbauc0YsbUF8qKMVeBZl0pFCunxBIhKN/WlxpZ3lBuo=
cosmossdk.io/core v0.6.1 h1:OBy7TI2W+/gyn2z40vVvruK3di+cAluinA6cybFbE7s=
cosmossdk.io/core v0.6.1/go.mod h1:g3MMBCBXtxbDWBURDVnJE7XML4BG5qENhs0gzkcpuFA=
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4 h1:l1scDTT2VX18ZuR6P0irvT/bAP0h4297D/Lka5nz2vE=
cosmossdk.io/core v0.6.2-0.20230323161322-ccd8d40119e4/go.mod h1:J8R0E7soOpQFVqFiFd7EKepXCPpINa2n2t2EqbEsXnY=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=
cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU=
cosmossdk.io/errors v1.0.0-beta.7 h1:gypHW76pTQGVnHKo6QBkb4yFOJjC+sUGRc5Al3Odj1w=

View File

@ -138,9 +138,6 @@ func (bm BasicManager) RegisterGRPCGatewayRoutes(clientCtx client.Context, rtr *
}
// AddTxCommands adds all tx commands to the rootTxCmd.
//
// TODO: Remove clientCtx argument.
// REF: https://github.com/cosmos/cosmos-sdk/issues/6571
func (bm BasicManager) AddTxCommands(rootTxCmd *cobra.Command) {
for _, b := range bm {
if cmd := b.GetTxCmd(); cmd != nil {
@ -150,9 +147,6 @@ func (bm BasicManager) AddTxCommands(rootTxCmd *cobra.Command) {
}
// AddQueryCommands adds all query commands to the rootQueryCmd.
//
// TODO: Remove clientCtx argument.
// REF: https://github.com/cosmos/cosmos-sdk/issues/6571
func (bm BasicManager) AddQueryCommands(rootQueryCmd *cobra.Command) {
for _, b := range bm {
if cmd := b.GetQueryCmd(); cmd != nil {

View File

@ -25,8 +25,6 @@ func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions {
},
},
},
Tx: &autocliv1.ServiceCommandDescriptor{
Service: authv1beta1.Msg_ServiceDesc.ServiceName,
},
// Tx is purposely left empty, as the only tx is MsgUpdateParams which is gov gated.
}
}

View File

@ -1,270 +0,0 @@
package cli
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"cosmossdk.io/core/address"
"cosmossdk.io/x/nft"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/version"
)
// Flag names and values
const (
FlagOwner = "owner"
FlagClassID = "class-id"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(ac address.Codec) *cobra.Command {
nftQueryCmd := &cobra.Command{
Use: nft.ModuleName,
Short: "Querying commands for the nft module",
Long: "",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
nftQueryCmd.AddCommand(
GetCmdQueryClass(),
GetCmdQueryClasses(),
GetCmdQueryNFT(),
GetCmdQueryNFTs(ac),
GetCmdQueryOwner(),
GetCmdQueryBalance(),
GetCmdQuerySupply(),
)
return nftQueryCmd
}
// GetCmdQueryClass implements the query class command.
func GetCmdQueryClass() *cobra.Command {
cmd := &cobra.Command{
Use: "class [class-id]",
Args: cobra.ExactArgs(1),
Short: "query an NFT class based on its id",
Example: fmt.Sprintf(`$ %s query %s class <class-id>`, version.AppName, nft.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
res, err := queryClient.Class(cmd.Context(), &nft.QueryClassRequest{
ClassId: args[0],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}
// GetCmdQueryClasses implements the query classes command.
func GetCmdQueryClasses() *cobra.Command {
cmd := &cobra.Command{
Use: "classes",
Short: "query all NFT classes",
Example: fmt.Sprintf(`$ %s query %s classes`, version.AppName, nft.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}
res, err := queryClient.Classes(cmd.Context(), &nft.QueryClassesRequest{
Pagination: pageReq,
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "classes")
return cmd
}
// GetCmdQueryNFT implements the query nft command.
func GetCmdQueryNFT() *cobra.Command {
cmd := &cobra.Command{
Use: "nft [class-id] [nft-id]",
Args: cobra.ExactArgs(2),
Short: "query an NFT based on its class and id.",
Example: fmt.Sprintf(`$ %s query %s nft`, version.AppName, nft.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
res, err := queryClient.NFT(cmd.Context(), &nft.QueryNFTRequest{
ClassId: args[0],
Id: args[1],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}
// GetCmdQueryNFTs implements the query nft command.
func GetCmdQueryNFTs(ac address.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "nfts",
Short: "query all NFTs of a given class or owner address.",
Long: strings.TrimSpace(
fmt.Sprintf(`Query all NFTs of a given class or owner address. If owner
is set, all nfts that belong to the owner are filtered out.
Examples:
$ %s query %s nfts <class-id> --owner=<owner>
`,
version.AppName, nft.ModuleName),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}
owner, err := cmd.Flags().GetString(FlagOwner)
if err != nil {
return err
}
if len(owner) > 0 {
if _, err := ac.StringToBytes(owner); err != nil {
return err
}
}
classID, err := cmd.Flags().GetString(FlagClassID)
if err != nil {
return err
}
if len(owner) == 0 && len(classID) == 0 {
return errors.ErrInvalidRequest.Wrap("must provide at least one of classID or owner")
}
request := &nft.QueryNFTsRequest{
ClassId: classID,
Owner: owner,
Pagination: pageReq,
}
res, err := queryClient.NFTs(cmd.Context(), request)
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "nfts")
cmd.Flags().String(FlagOwner, "", "The owner of the nft")
cmd.Flags().String(FlagClassID, "", "The class-id of the nft")
return cmd
}
// GetCmdQueryOwner implements the query owner command.
func GetCmdQueryOwner() *cobra.Command {
cmd := &cobra.Command{
Use: "owner [class-id] [nft-id]",
Args: cobra.ExactArgs(2),
Short: "query the owner of the NFT based on its class and id.",
Example: fmt.Sprintf(`$ %s query %s owner <class-id> <nft-id>`, version.AppName, nft.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
res, err := queryClient.Owner(cmd.Context(), &nft.QueryOwnerRequest{
ClassId: args[0],
Id: args[1],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}
// GetCmdQueryBalance implements the query balance command.
func GetCmdQueryBalance() *cobra.Command {
cmd := &cobra.Command{
Use: "balance [owner] [class-id]",
Args: cobra.ExactArgs(2),
Short: "query the number of NFTs of a given class owned by the owner.",
Example: fmt.Sprintf(`$ %s query %s balance <owner> <class-id>`, version.AppName, nft.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
res, err := queryClient.Balance(cmd.Context(), &nft.QueryBalanceRequest{
ClassId: args[1],
Owner: args[0],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}
// GetCmdQuerySupply implements the query supply command.
func GetCmdQuerySupply() *cobra.Command {
cmd := &cobra.Command{
Use: "supply [class-id]",
Args: cobra.ExactArgs(1),
Short: "query the number of nft based on the class.",
Example: fmt.Sprintf(`$ %s query %s supply <class-id>`, version.AppName, nft.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := nft.NewQueryClient(clientCtx)
res, err := queryClient.Supply(cmd.Context(), &nft.QuerySupplyRequest{
ClassId: args[0],
})
if err != nil {
return err
}
return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
return cmd
}

View File

@ -1,308 +0,0 @@
package cli_test
import (
"context"
"fmt"
"io"
"cosmossdk.io/x/nft"
"cosmossdk.io/x/nft/client/cli"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec/address"
svrcmd "github.com/cosmos/cosmos-sdk/server/cmd"
"github.com/cosmos/cosmos-sdk/testutil"
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
)
func (s *CLITestSuite) TestQueryClass() {
testCases := []struct {
name string
args []string
expCmdOutput string
}{
{
name: "json output",
args: []string{testClassID, fmt.Sprintf("--%s=json", flags.FlagOutput)},
expCmdOutput: `[kitty --output=json]`,
},
{
name: "text output",
args: []string{testClassID, fmt.Sprintf("--%s=text", flags.FlagOutput)},
expCmdOutput: `[kitty --output=text]`,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryClass()
ctx := svrcmd.CreateExecuteContext(context.Background())
cmd.SetOut(io.Discard)
s.Require().NotNil(cmd)
cmd.SetContext(ctx)
cmd.SetArgs(tc.args)
s.Require().NoError(client.SetCmdClientContextHandler(s.baseCtx, cmd))
s.Require().Contains(fmt.Sprint(cmd), "class [class-id] [] [] query an NFT class based on its id")
s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput)
_, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args)
s.Require().NoError(err)
})
}
}
func (s *CLITestSuite) TestQueryClasses() {
testCases := []struct {
name string
flagArgs []string
expCmdOutput string
}{
{
name: "json output",
flagArgs: []string{fmt.Sprintf("--%s=json", flags.FlagOutput)},
expCmdOutput: `[--output=json]`,
},
{
name: "text output",
flagArgs: []string{fmt.Sprintf("--%s=text", flags.FlagOutput)},
expCmdOutput: `[--output=text]`,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryClasses()
ctx := svrcmd.CreateExecuteContext(context.Background())
cmd.SetOut(io.Discard)
s.Require().NotNil(cmd)
cmd.SetContext(ctx)
cmd.SetArgs(tc.flagArgs)
s.Require().NoError(client.SetCmdClientContextHandler(s.baseCtx, cmd))
s.Require().Contains(fmt.Sprint(cmd), "classes [] [] query all NFT classes")
s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput)
_, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.flagArgs)
s.Require().NoError(err)
})
}
}
func (s *CLITestSuite) TestQueryNFT() {
testCases := []struct {
name string
args []string
expCmdOutput string
}{
{
name: "json output",
args: []string{testClassID, testID, fmt.Sprintf("--%s=json", flags.FlagOutput)},
expCmdOutput: `[kitty kitty1 --output=json]`,
},
{
name: "text output",
args: []string{testClassID, testID, fmt.Sprintf("--%s=text", flags.FlagOutput)},
expCmdOutput: `[kitty kitty1 --output=text]`,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryNFT()
ctx := svrcmd.CreateExecuteContext(context.Background())
cmd.SetOut(io.Discard)
s.Require().NotNil(cmd)
cmd.SetContext(ctx)
cmd.SetArgs(tc.args)
s.Require().NoError(client.SetCmdClientContextHandler(s.baseCtx, cmd))
s.Require().Contains(fmt.Sprint(cmd), "nft [class-id] [nft-id] [] [] query an NFT based on its class and id")
s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput)
_, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args)
s.Require().NoError(err)
})
}
}
func (s *CLITestSuite) TestQueryNFTs() {
accounts := testutil.CreateKeyringAccounts(s.T(), s.kr, 1)
testCases := []struct {
name string
args struct {
ClassID string
Owner string
}
expectErr bool
expErrMsg string
}{
{
name: "empty class id and owner",
args: struct {
ClassID string
Owner string
}{},
expectErr: true,
expErrMsg: "must provide at least one of classID or owner",
},
{
name: "valid case",
args: struct {
ClassID string
Owner string
}{
ClassID: testClassID,
Owner: accounts[0].Address.String(),
},
expectErr: false,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryNFTs(address.NewBech32Codec("cosmos"))
var args []string
args = append(args, fmt.Sprintf("--%s=%s", cli.FlagClassID, tc.args.ClassID))
args = append(args, fmt.Sprintf("--%s=%s", cli.FlagOwner, tc.args.Owner))
args = append(args, fmt.Sprintf("--%s=json", flags.FlagOutput))
out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, args)
if tc.expectErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expErrMsg)
} else {
s.Require().NoError(err)
var result nft.QueryNFTsResponse
err = s.clientCtx.Codec.UnmarshalJSON(out.Bytes(), &result)
s.Require().NoError(err)
}
})
}
}
func (s *CLITestSuite) TestQueryOwner() {
testCases := []struct {
name string
args []string
expCmdOutput string
}{
{
name: "json output",
args: []string{testClassID, testID, fmt.Sprintf("--%s=json", flags.FlagOutput)},
expCmdOutput: `[kitty kitty1 --output=json]`,
},
{
name: "text output",
args: []string{testClassID, testID, fmt.Sprintf("--%s=text", flags.FlagOutput)},
expCmdOutput: `[kitty kitty1 --output=text]`,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryOwner()
ctx := svrcmd.CreateExecuteContext(context.Background())
cmd.SetOut(io.Discard)
s.Require().NotNil(cmd)
cmd.SetContext(ctx)
cmd.SetArgs(tc.args)
s.Require().NoError(client.SetCmdClientContextHandler(s.baseCtx, cmd))
s.Require().Contains(fmt.Sprint(cmd), "owner [class-id] [nft-id] [] [] query the owner of the NFT based on its class and id")
s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput)
_, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args)
s.Require().NoError(err)
})
}
}
func (s *CLITestSuite) TestQueryBalance() {
accounts := testutil.CreateKeyringAccounts(s.T(), s.kr, 1)
testCases := []struct {
name string
args []string
expCmdOutput string
}{
{
name: "json output",
args: []string{accounts[0].Address.String(), testClassID, fmt.Sprintf("--%s=json", flags.FlagOutput)},
expCmdOutput: fmt.Sprintf("%s kitty --output=json", accounts[0].Address.String()),
},
{
name: "text output",
args: []string{accounts[0].Address.String(), testClassID, fmt.Sprintf("--%s=text", flags.FlagOutput)},
expCmdOutput: fmt.Sprintf("%s kitty --output=text", accounts[0].Address.String()),
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQueryBalance()
ctx := svrcmd.CreateExecuteContext(context.Background())
cmd.SetOut(io.Discard)
s.Require().NotNil(cmd)
cmd.SetContext(ctx)
cmd.SetArgs(tc.args)
s.Require().NoError(client.SetCmdClientContextHandler(s.baseCtx, cmd))
s.Require().Contains(fmt.Sprint(cmd), "balance [owner] [class-id] [] [] query the number of NFTs of a given class owned by the owner")
s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput)
_, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args)
s.Require().NoError(err)
})
}
}
func (s *CLITestSuite) TestQuerySupply() {
testCases := []struct {
name string
args []string
expCmdOutput string
}{
{
name: "valid case",
args: []string{testClassID, fmt.Sprintf("--%s=json", flags.FlagOutput)},
expCmdOutput: `[kitty --output=json]`,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
cmd := cli.GetCmdQuerySupply()
ctx := svrcmd.CreateExecuteContext(context.Background())
cmd.SetOut(io.Discard)
s.Require().NotNil(cmd)
cmd.SetContext(ctx)
cmd.SetArgs(tc.args)
s.Require().NoError(client.SetCmdClientContextHandler(s.baseCtx, cmd))
s.Require().Contains(fmt.Sprint(cmd), "supply [class-id] [] [] query the number of nft based on the class")
s.Require().Contains(fmt.Sprint(cmd), tc.expCmdOutput)
_, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args)
s.Require().NoError(err)
})
}
}

87
x/nft/module/autocli.go Normal file
View File

@ -0,0 +1,87 @@
package module
import (
"fmt"
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
nftv1beta1 "cosmossdk.io/api/cosmos/nft/v1beta1"
"cosmossdk.io/x/nft"
"github.com/cosmos/cosmos-sdk/version"
)
// AutoCLIOptions implements the autocli.HasAutoCLIConfig interface.
func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions {
return &autocliv1.ModuleOptions{
Query: &autocliv1.ServiceCommandDescriptor{
Service: nftv1beta1.Query_ServiceDesc.ServiceName,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{
RpcMethod: "Balance",
Use: "balance [owner] [class-id]",
Short: "Query the number of NFTs of a given class owned by the owner.",
Example: fmt.Sprintf(`%s query %s balance <owner> <class-id>`, version.AppName, nft.ModuleName),
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{ProtoField: "owner"},
{ProtoField: "class_id"},
},
},
{
RpcMethod: "Owner",
Use: "owner [class-id] [nft-id]",
Short: "Query the owner of the NFT based on its class and id.",
Example: fmt.Sprintf(`%s query %s owner <class-id> <nft-id>`, version.AppName, nft.ModuleName),
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{ProtoField: "class_id"},
{ProtoField: "id"},
},
},
{
RpcMethod: "Supply",
Use: "supply [class-id]",
Short: "Query the number of nft based on the class.",
Example: fmt.Sprintf(`%s query %s supply <class-id>`, version.AppName, nft.ModuleName),
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{ProtoField: "class_id"},
},
},
{
RpcMethod: "NFTs",
Use: "nfts [class-id]",
Short: "Query all NFTs of a given class or owner address.",
Example: fmt.Sprintf(`%s query %s nfts <class-id> --owner=<owner>`, version.AppName, nft.ModuleName),
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{ProtoField: "class_id"},
},
},
{
RpcMethod: "NFT",
Use: "nft [class-id] [nft-id]",
Short: "Query an NFT based on its class and id.",
Example: fmt.Sprintf(`%s query %s nft <class-id> <nft-id>`, version.AppName, nft.ModuleName),
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{ProtoField: "class_id"},
{ProtoField: "id"},
},
},
{
RpcMethod: "Class",
Use: "class [class-id]",
Short: "Query an NFT class based on its id",
Example: fmt.Sprintf(`%s query %s class <class-id>`, version.AppName, nft.ModuleName),
PositionalArgs: []*autocliv1.PositionalArgDescriptor{
{ProtoField: "class_id"},
},
},
{
RpcMethod: "Classes",
Use: "classes",
Short: "Query all NFT classes.",
Example: fmt.Sprintf(`%s query %s classes`, version.AppName, nft.ModuleName),
},
},
},
Tx: &autocliv1.ServiceCommandDescriptor{
Service: nftv1beta1.Msg_ServiceDesc.ServiceName,
},
}
}

View File

@ -86,9 +86,10 @@ func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx sdkclient.Context, mux
}
}
// GetQueryCmd returns the cli query commands for the nft module
// GetQueryCmd returns a no-op command for the nft module.
// Queries for NFT are registered by autocli.
func (ab AppModuleBasic) GetQueryCmd() *cobra.Command {
return cli.GetQueryCmd(ab.ac)
return nil
}
// GetTxCmd returns the transaction commands for the nft module