order | synopsis |
---|---|
11 |
This document details how to build CLI and REST interfaces for a module. Examples from various SDK modules are included. |
- Building Modules Intro {prereq}
One of the main interfaces for an application is the command-line interface. This entrypoint adds commands from the application's modules to let end-users create messages and queries. The CLI files are typically found in the ./x/moduleName/client/cli
folder.
Transactions are created by users to wrap messages that trigger state changes when they get included in a valid block. Transaction commands typically have their own tx.go
file in the module ./x/moduleName/client/cli
folder. The commands are specified in getter functions prefixed with GetCmd
and include the name of the command.
Here is an example from the nameservice
module:
This getter function creates the command for the Buy Name transaction. It does the following:
- Construct the command: Read the Cobra Documentation for details on how to create commands.
- Use: Specifies the format of a command-line entry users should type in order to invoke this command. In this case, the user uses
buy-name
as the name of the transaction command and provides thename
the user wishes to buy and theamount
the user is willing to pay. - Args: The number of arguments the user provides, in this case exactly two:
name
andamount
. - Short and Long: A description for the function is provided here. A
Short
description is expected, andLong
can be used to provide a more detailed description when a user uses the--help
flag to ask for more information. - RunE: Defines a function that can return an error, called when the command is executed. Using
Run
would do the same thing, but would not allow for errors to be returned.
- Use: Specifies the format of a command-line entry users should type in order to invoke this command. In this case, the user uses
RunE
Function Body: The function should be specified as aRunE
to allow for errors to be returned. This function encapsulates all of the logic to create a new transaction that is ready to be relayed to nodes.- The function should first initialize a
TxBuilder
with the applicationcodec
'sTxEncoder
, as well as a newCLIContext
with thecodec
andAccountDecoder
. These contexts contain all the information provided by the user and will be used to transfer this user-specific information between processes. To learn more about how contexts are used in a transaction, click here. - If applicable, the command's arguments are parsed. Here, the
amount
given by the user is parsed into a denomination ofcoins
. - If applicable, the
CLIContext
is used to retrieve any parameters such as the transaction originator's address to be used in the transaction. Here, thefrom
address is retrieved by callingcliCtx.getFromAddress()
. - A message is created using all parameters parsed from the command arguments and
CLIContext
. The constructor function of the specific message type is called directly. It is good practice to callValidateBasic()
on the newly created message to run a sanity check and check for invalid arguments. - Depending on what the user wants, the transaction is either generated offline or signed and broadcasted to the preconfigured node using
GenerateOrBroadcastMsgs()
.
- The function should first initialize a
- Flags. Add any flags to the command. No flags were specified here, but all transaction commands have flags to provide additional information from the user (e.g. amount of fees they are willing to pay). These persistent transaction flags can be added to a higher-level command so that they apply to all transaction commands.
Finally, the module needs to have a GetTxCmd()
, which aggregates all of the transaction commands of the module. Often, each command getter function has its own file in the module's cli
folder, and a separate tx.go
file contains GetTxCmd()
. Application developers wishing to include the module's transactions will call this function to add them as subcommands in their CLI. Here is the auth
GetTxCmd()
function, which adds the Sign
and MultiSign
commands.
An application using this module likely adds auth
module commands to its root TxCmd
command by calling txCmd.AddCommand(authModuleClient.GetTxCmd())
.
Queries allow users to gather information about the application or network state; they are routed by the application and processed by the module in which they are defined. Query commands typically have their own query.go
file in the module x/moduleName/client/cli
folder. Like transaction commands, they are specified in getter functions and have the prefix GetCmdQuery
. Here is an example of a query command from the nameservice
module:
This query returns the address that owns a particular name. The getter function does the following:
codec
andqueryRoute
. In addition to taking in the applicationcodec
, query command getters also take aqueryRoute
used to construct a path Baseapp uses to route the query in the application.- Construct the command. Read the Cobra Documentation and the transaction command example above for more information. The user must type
whois
and provide thename
they are querying for as the only argument. RunE
. The function should be specified as aRunE
to allow for errors to be returned. This function encapsulates all of the logic to create a new query that is ready to be relayed to nodes.- The function should first initialize a new
CLIContext
with the applicationcodec
. - If applicable, the
CLIContext
is used to retrieve any parameters (e.g. the query originator's address to be used in the query) and marshal them with the query parameter type, in preparation to be relayed to a node. There are noCLIContext
parameters in this case because the query does not involve any information about the user. - The
queryRoute
is used to construct apath
baseapp
will use to route the query to the appropriate querier. The expected format for a querypath
is "queryCategory/queryRoute/queryType/arg1/arg2/...", wherequeryCategory
can bep2p
,store
,app
, orcustom
,queryRoute
is the name of the module, andqueryType
is the name of the query type defined within the module.baseapp
can handle each type ofqueryCategory
by routing it to a module querier or retrieving results directly from stores and functions for querying peer nodes. Module queries arecustom
type queries (some SDK modules have exceptions, such asauth
andgov
module queries). - The
CLIContext
QueryWithData()
function is used to relay the query to a node and retrieve the response. It requires thepath
. It returns the result and height of the query upon success or an error if the query fails. In addition, it will verify the returned proof ifTrustNode
is disabled. If proof verification fails or the query height is invalid, an error will be returned. - The
codec
is used to nmarshal the response and theCLIContext
is used to print the output back to the user.
- The function should first initialize a new
- Flags. Add any flags to the command.
Finally, the module also needs a GetQueryCmd
, which aggregates all of the query commands of the module. Application developers wishing to include the module's queries will call this function to add them as subcommands in their CLI. Its structure is identical to the GetTxCmd
command shown above.
Flags are entered by the user and allow for command customizations. Examples include the fees or gas prices users are willing to pay for their transactions.
The flags for a module are typically found in a flags.go
file in the ./x/moduleName/client/cli
folder. Module developers can create a list of possible flags including the value type, default value, and a description displayed if the user uses a help
command. In each transaction getter function, they can add flags to the commands and, optionally, mark flags as required so that an error is thrown if the user does not provide values for them.
For full details on flags, visit the Cobra Documentation.
For example, the SDK ./client/flags
package includes a PostCommands()
function that adds necessary flags to transaction commands, such as the from
flag to indicate which address the transaction originates from.
Here is an example of how to add a flag using the from
flag from this function.
cmd.Flags().String(FlagFrom, "", "Name or address of private key with which to sign")
The input provided for this flag - called FlagFrom
is a string with the default value of ""
if none is provided. If the user asks for a description of this flag, the description will be printed.
A flag can be marked as required so that an error is automatically thrown if the user does not provide a value:
cmd.MarkFlagRequired(FlagFrom)
Since PostCommands()
includes all of the basic flags required for a transaction command, module developers may choose not to add any of their own (specifying arguments instead may often be more appropriate). For a full list of what flags are included in the PostCommands()
function, including which are required inputs from users, see the CLI documentation here.
Applications typically support web services that use HTTP requests (e.g. a web wallet like Lunie.io. Thus, application developers will also use REST Routes to route HTTP requests to the application's modules; these routes will be used by service providers. The module developer's responsibility is to define the REST client by defining routes for all possible requests and handlers for each of them. It's up to the module developer how to organize the REST interface files; there is typically a rest.go
file found in the module's ./x/moduleName/client/rest
folder.
To support HTTP requests, the module developer needs to define possible request types, how to handle them, and provide a way to register them with a provided router.
Request types, which define structured interactions from users, must be defined for all transaction requests. Users using this method to interact with an application will send HTTP Requests with the required fields in order to trigger state changes in the application. Conventionally, each request is named with the suffix Req
, e.g. SendReq
for a Send transaction. Each struct should include a base request baseReq
, the name of the transaction, and all the arguments the user must provide for the transaction.
Here is an example of a request to buy a name from the nameservice
module:
The BaseReq
includes basic information that every request needs to have, similar to required flags in a CLI. All of these values, including GasPrices
and AccountNumber
, will be provided in the request body. The user will also need to specify the arguments Name
and Amount
fields in the body and Buyer
will be provided by the user's address.
BaseReq
is a type defined in the SDK that encapsulates much of the transaction configurations similar to CLI command flags. Users must provide the information in the body of their requests.
From
indicates which account the transaction originates from. This account is used to sign the transaction.Memo
sends a memo along with the transaction.ChainID
specifies the unique identifier of the blockchain the transaction pertains to.AccountNumber
is an identifier for the account.Sequence
is the value of a counter measuring how many transactions have been sent from the account. It is used to prevent replay attacks.Gas
refers to how much gas, which represents computational resources, Tx consumes. Gas is dependent on the transaction and is not precisely calculated until execution, but can be estimated by providing auto as the value forGas
.GasAdjustment
can be used to scale gas up in order to avoid underestimating. For example, users can specify their gas adjustment as 1.5 to use 1.5 times the estimated gas.GasPrices
specifies how much the user is willing pay per unit of gas, which can be one or multiple denominations of tokens. For example, --gas-prices=0.025uatom, 0.025upho means the user is willing to pay 0.025uatom AND 0.025upho per unit of gas.Fees
specifies how much in fees the user is willing to pay in total. Note that the user only needs to provide eithergas-prices
orfees
, but not both, because they can be derived from each other.Simulate
instructs the application to ignore gas and simulate the transaction running without broadcasting.
Request handlers must be defined for both transaction and query requests. Handlers' arguments include a reference to the application's codec
and the CLIContext
created in the user interaction.
Here is an example of a request handler for the nameservice module buyNameReq
request (the same one shown above):
The request handler can be broken down as follows:
- Parse Request: The request handler first attempts to parse the request, and then run
Sanitize
andValidateBasic
on the underlyingBaseReq
to check the validity of the request. Next, it attempts to parse the argumentsBuyer
andAmount
to the typesAccountAddress
andCoins
respectively. - Message: Then, a message of the type
MsgBuyName
(defined by the module developer to trigger the state changes for this transaction) is created from the values and another sanity check,ValidateBasic
is run on it. - Generate Transaction: Finally, the HTTP
ResponseWriter
, applicationcodec
,CLIContext
, requestBaseReq
, and message is passed toWriteGenerateStdTxResponse
to further process the request.
To read more about how a transaction is generated, visit the transactions documentation here.
The application CLI entrypoint will have a RegisterRoutes
function in its main.go
file, which calls the registerRoutes
functions of each module utilized by the application. Module developers need to implement registerRoutes
for their modules so that applications are able to route messages and queries to their corresponding handlers and queriers.
The router used by the SDK is Gorilla Mux. The router is initialized with the Gorilla Mux NewRouter()
function. Then, the router's HandleFunc
function can then be used to route urls with the defined request handlers and the HTTP method (e.g. "POST", "GET") as a route matcher. It is recommended to prefix every route with the name of the module to avoid collisions with other modules that have the same query or transaction names.
Here is a registerRoutes
function with one query route example from the nameservice tutorial:
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) {
// ResolveName Query
r.HandleFunc(fmt.Sprintf("/%s/names/{%s}", storeName, restName), resolveNameHandler(cdc, cliCtx, storeName)).Methods("GET")
}
A few things to note:
- The router
r
has already been initialized by the application and is passed in here as an argument - this function is able to add on the nameservice module's routes onto any application's router. The application must also provide aCLIContext
that the querier will need to process user requests and the applicationcodec
for encoding and decoding application-specific types. "/%s/names/{%s}", storeName, restName
is the url for the HTTP request.storeName
is the name of the module,restName
is a variable provided by the user to specify what kind of query they are making.resolveNameHandler
is the query request handler defined by the module developer. It also takes the applicationcodec
andCLIContext
passed in from the user side, as well as thestoreName
."GET"
is the HTTP Request method. As to be expected, queries are typically GET requests. Transactions are typically POST and PUT requests.
Read about the recommended module structure {hide}