This document describes how to create a commmand-line interface (CLI) for an application. A separate document for implementing a CLI for an SDK module can be found here. {synopsis}
- Lifecycle of a Query {prereq}
One of the main entrypoints of an application is the command-line interface. This entrypoint is created via a main.go
file which compiles to a binary, conventionally placed in the application's ./cmd/cli
folder. The CLI for an application is typically be referred to as the name of the application suffixed with -cli
, e.g. appcli
. Here is where the interfaces docs lie in the directory from the nameservice tutorial.
There is no set way to create a CLI, but SDK modules typically use the Cobra Library. Building a CLI with Cobra entails defining commands, arguments, and flags. Commands understand the actions users wish to take, such as tx
for creating a transaction and query
for querying the application. Each command can also have nested subcommands, necessary for naming the specific transaction type. Users also supply Arguments, such as account numbers to send coins to, and Flags to modify various aspects of the commands, such as gas prices or which node to broadcast to.
Here is an example of a command a user might enter to interact with the nameservice CLI nscli
in order to buy a name:
nscli tx nameservice buy-name <name> <amount> --gas auto --gas-prices <gasPrices>
The first four strings specify the command:
- The root command for the entire application
nscli
. - The subcommand
tx
, which contains all commands that let users create transactions. - The subcommand
nameservice
to indicate which module to route the command to (nameservice
module in this case). - The type of transaction
buy-name
.
The next two strings are arguments: the name
the user wishes to buy and the amount
they want to pay for it. Finally, the last few strings of the command are flags to indicate how much the user is willing to pay in fees (calculated using the amount of gas used to execute the transaction and the gas prices provided by the user).
The CLI interacts with a node (running nsd
) to handle this command. The interface itself is defined in a main.go
file.
The main.go
file needs to have a main()
function that does the following to run the command-line interface:
- Instantiate the
codec
by calling the application'sMakeCodec()
function. Thecodec
is used to code and encode data structures for the application - stores can only persist[]byte
s so the developer must define a serialization format for their data structures or use the default, Amino. - Configurations are set by reading in configuration files (e.g. the sdk config file).
- Create the root command to which all the application commands will be added as subcommands and add any required flags to it, such as
--chain-id
. - Add subcommands for all the possible user interactions, including transaction commands and query commands.
- Create an Executor and execute the root command.
See an example of main()
function from the nameservice
application:
The rest of the document will detail what needs to be implemented for each step and include smaller portions of code from the nameservice CLI main.go
file.
Every application CLI first constructs a root command, then adds functionality by aggregating subcommands (often with further nested subcommands) using rootCmd.AddCommand()
. The bulk of an application's unique capabilities lies in its transaction and query commands, called TxCmd
and QueryCmd
respectively.
The root command (called rootCmd
) is what the user first types into the command line to indicate which application they wish to interact with. The string used to invoke the command (the "Use" field) is typically the name of the application suffixed with -cli
, e.g. appcli
. The root command typically includes the following commands to support basic functionality in the application.
- Status command from the SDK rpc client tools, which prints information about the status of the connected
Node
. The Status of a node includesNodeInfo
,SyncInfo
andValidatorInfo
. - Config command from the SDK client tools, which allows the user to edit a
config.toml
file that sets values for flags such as--chain-id
and which--node
they wish to connect to. Theconfig
command can be invoked by typingappcli config
with optional arguments<key> [value]
and a--get
flag to query configurations or--home
flag to create a new configuration. - Keys commands from the SDK client tools, which includes a collection of subcommands for using the key functions in the SDK crypto tools, including adding a new key and saving it to disk, listing all public keys stored in the key manager, and deleting a key. For example, users can type
appcli keys add <name>
to add a new key and save an encrypted copy to disk, using the flag--recover
to recover a private key from a seed phrase or the flag--multisig
to group multiple keys together to create a multisig key. For full details on theadd
key command, see the code here. For more details about usage of--keyring-backend
for storage of key credentials look at the keyring docs. - Transaction commands.
- Query commands.
Next is an example main()
function from the nameservice
application. It instantiates the root command, adds a persistent flag and PreRun
function to be run before every execution, and adds all of the necessary subcommands.
The root-level status
, config
, and keys
subcommands are common across most applications and do not interact with application state. The bulk of an application's functionality - what users can actually do with it - is enabled by its transaction commands.
Transactions are objects wrapping messages that trigger state changes. To enable the creation of transactions using the CLI interface, a function txCmd
is generally added to the rootCmd
:
This txCmd
function adds all the transaction available to end-users for the application. This typically includes:
- Sign command from the
auth
module that signs messages in a transaction. To enable multisig, add theauth
module'sMultiSign
command. Since every transaction requires some sort of signature in order to be valid, thithe signing command is necessary for every application. - Broadcast command from the SDK client tools, to broadcast transactions.
- Send command from the
bank
module, which is a transaction that allows accounts to send coins to one another, including gas and fees for transactions. - All module transaction commands the application is dependent on, retrieved by using the basic module manager's
AddTxCommands()
function.
Here is an example of a txCmd
aggregating these subcommands from the nameservice
application:
Queries are objects that allow users to retrieve information about the application's state. To enable the creation of transactions using the CLI interface, a function txCmd
is generally added to the rootCmd
:
This queryCmd
function adds all the queries available to end-users for the application. This typically includes:
- QueryTx and/or other transaction query commands] from the
auth
module which allow the user to search for a transaction by inputting its hash, a list of tags, or a block height. These queries allow users to see if transactions have been included in a block. - Account command from the
auth
module, which displays the state (e.g. account balance) of an account given an address. - Validator command from the SDK rpc client tools, which displays the validator set of a given height.
- Block command from the SDK rpc client tools, which displays the block data for a given height.
- All module query commands the application is dependent on, retrieved by using the basic module manager's
AddQueryCommands()
function.
Here is an example of a queryCmd
aggregating subcommands from the nameservice
application:
Flags are used to modify commands; developers can include them in a flags.go
file with their CLI. Users can explicitly include them in commands or pre-configure them by entering a command in the format appcli config <flag> <value>
into their command line. Commonly pre-configured flags include the --node
to connect to and --chain-id
of the blockchain the user wishes to interact with.
A persistent flag (as opposed to a local flag) added to a command transcends all of its children: subcommands will inherit the configured values for these flags. Additionally, all flags have default values when they are added to commands; some toggle an option off but others are empty values that the user needs to override to create valid commands. A flag can be explicitly marked as required so that an error is automatically thrown if the user does not provide a value, but it is also acceptable to handle unexpected missing flags differently.
Flags are added to commands directly (generally in the module's CLI file where module commands are defined) and no flag except for the rootCmd
persistent flags has to be added at application level. It is common to add a persistent flag for --chain-id
, the unique identifier of the blockchain the application pertains to, to the root command. Adding this flag can be done in the main()
function. Adding this flag makes sense as the chain ID should not be changing across commands in this application CLI. Here is an example from the nameservice
application:
The last function to define in main.go
is initConfig
, which does exactly what it sounds like - initialize configurations. To call this function, set it as a PersistentPreRunE
function for the root command, so that it always executes before the main execution of the root command and any of its subcommands. initConfig()
does the following:
- Read in the
config.toml
file. This same file is edited throughconfig
commands. - Use the Viper to read in configurations from the file and set them.
- Set any persistent flags defined by the user:
--chain-id
,--encoding
,--output
, etc.
Here is an example of an initConfig()
function from the nameservice tutorial CLI:
And an example of how to add initConfig
as a PersistentPreRunE
to the root command: