diff --git a/PENDING.md b/PENDING.md index ae07da0bd..b3ed20c61 100644 --- a/PENDING.md +++ b/PENDING.md @@ -3,6 +3,7 @@ BREAKING CHANGES * Gaia REST API (`gaiacli advanced rest-server`) + * [gaia-lite] [\#2819](https://github.com/cosmos/cosmos-sdk/pull/2819) Txs query param format is now: `/txs?tag=value` (removed '' wrapping the query parameter `value`) * Gaia CLI (`gaiacli`) * [cli] [\#2728](https://github.com/cosmos/cosmos-sdk/pull/2728) Seperate `tx` and `query` subcommands by module @@ -56,10 +57,12 @@ FEATURES IMPROVEMENTS * Gaia REST API (`gaiacli advanced rest-server`) + * [gaia-lite] [\#2819](https://github.com/cosmos/cosmos-sdk/pull/2819) Tx search now supports multiple tags as query parameters * [\#2836](https://github.com/cosmos/cosmos-sdk/pull/2836) Expose LCD router to allow users to register routes there. * Gaia CLI (`gaiacli`) * [\#2749](https://github.com/cosmos/cosmos-sdk/pull/2749) Add --chain-id flag to gaiad testnet + * [\#2819](https://github.com/cosmos/cosmos-sdk/pull/2819) Tx search now supports multiple tags as query parameters * Gaia - #2772 Update BaseApp to not persist state when the ante handler fails on DeliverTx. @@ -78,7 +81,7 @@ IMPROVEMENTS - #2779 Introduce `ValidateBasic` to the `Tx` interface and call it in the ante handler. - #2825 More staking and distribution invariants - * #2912 Print commit ID in hex when commit is synced. + - #2912 Print commit ID in hex when commit is synced. * Tendermint - #2796 Update to go-amino 0.14.1 @@ -95,7 +98,7 @@ BUG FIXES * Gaia * [\#2723] Use `cosmosvalcons` Bech32 prefix in `tendermint show-address` * [\#2742](https://github.com/cosmos/cosmos-sdk/issues/2742) Fix time format of TimeoutCommit override - * [\#2898](https://github.com/cosmos/cosmos-sdk/issues/2898) Remove redundant '$' in docker-compose.yml + * [\#2898](https://github.com/cosmos/cosmos-sdk/issues/2898) Remove redundant '$' in docker-compose.yml * SDK diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 001cf3d92..f6c2a4477 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "regexp" + "strings" "testing" "time" @@ -399,57 +400,39 @@ func TestTxs(t *testing.T) { cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) defer cleanup() - // query wrong - res, body := Request(t, port, "GET", "/txs", nil) - require.Equal(t, http.StatusBadRequest, res.StatusCode, body) + var emptyTxs []tx.Info + txs := getTransactions(t, port) + require.Equal(t, emptyTxs, txs) // query empty - res, body = Request(t, port, "GET", fmt.Sprintf("/txs?tag=sender_bech32='%s'", "cosmos1jawd35d9aq4u76sr3fjalmcqc8hqygs90d0g0v"), nil) - require.Equal(t, http.StatusOK, res.StatusCode, body) - require.Equal(t, "[]", body) + txs = getTransactions(t, port, fmt.Sprintf("sender=%s", addr.String())) + require.Equal(t, emptyTxs, txs) - // create TX + // also tests url decoding + txs = getTransactions(t, port, fmt.Sprintf("sender=%s", addr.String())) + require.Equal(t, emptyTxs, txs) + + txs = getTransactions(t, port, fmt.Sprintf("action=submit%%20proposal&proposer=%s", addr.String())) + require.Equal(t, emptyTxs, txs) + + // create tx receiveAddr, resultTx := doSend(t, port, seed, name, password, addr) - tests.WaitForHeight(resultTx.Height+1, port) - // check if tx is findable - res, body = Request(t, port, "GET", fmt.Sprintf("/txs/%s", resultTx.Hash), nil) - require.Equal(t, http.StatusOK, res.StatusCode, body) - - var indexedTxs []tx.Info - // check if tx is queryable - res, body = Request(t, port, "GET", fmt.Sprintf("/txs?tag=tx.hash='%s'", resultTx.Hash), nil) - require.Equal(t, http.StatusOK, res.StatusCode, body) - require.NotEqual(t, "[]", body) - - err := cdc.UnmarshalJSON([]byte(body), &indexedTxs) - require.NoError(t, err) - require.Equal(t, 1, len(indexedTxs)) - - // XXX should this move into some other testfile for txs in general? - // test if created TX hash is the correct hash - require.Equal(t, resultTx.Hash, indexedTxs[0].Hash) + txs = getTransactions(t, port, fmt.Sprintf("tx.hash=%s", resultTx.Hash)) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Hash, txs[0].Hash) // query sender - // also tests url decoding - res, body = Request(t, port, "GET", fmt.Sprintf("/txs?tag=sender_bech32=%%27%s%%27", addr), nil) - require.Equal(t, http.StatusOK, res.StatusCode, body) - - err = cdc.UnmarshalJSON([]byte(body), &indexedTxs) - require.NoError(t, err) - require.Equal(t, 1, len(indexedTxs), "%v", indexedTxs) // there are 2 txs created with doSend - require.Equal(t, resultTx.Height, indexedTxs[0].Height) + txs = getTransactions(t, port, fmt.Sprintf("sender=%s", addr.String())) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) // query recipient - res, body = Request(t, port, "GET", fmt.Sprintf("/txs?tag=recipient_bech32='%s'", receiveAddr), nil) - require.Equal(t, http.StatusOK, res.StatusCode, body) - - err = cdc.UnmarshalJSON([]byte(body), &indexedTxs) - require.NoError(t, err) - require.Equal(t, 1, len(indexedTxs)) - require.Equal(t, resultTx.Height, indexedTxs[0].Height) + txs = getTransactions(t, port, fmt.Sprintf("recipient=%s", receiveAddr.String())) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) } func TestPoolParamsQuery(t *testing.T) { @@ -534,6 +517,14 @@ func TestBonding(t *testing.T) { require.Equal(t, uint32(0), resultTx.CheckTx.Code) require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + // query tx + txs := getTransactions(t, port, + fmt.Sprintf("action=delegate&delegator=%s", addr), + fmt.Sprintf("destination-validator=%s", operAddrs[0]), + ) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) + acc := getAccount(t, port, addr) coins := acc.GetCoins() @@ -571,6 +562,14 @@ func TestBonding(t *testing.T) { coins = acc.GetCoins() require.Equal(t, int64(40), coins.AmountOf(stakeTypes.DefaultBondDenom).Int64()) + // query tx + txs = getTransactions(t, port, + fmt.Sprintf("action=begin-unbonding&delegator=%s", addr), + fmt.Sprintf("source-validator=%s", operAddrs[0]), + ) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) + unbonding := getUndelegation(t, port, addr, operAddrs[0]) require.Equal(t, "30", unbonding.Balance.Amount.String()) @@ -581,6 +580,15 @@ func TestBonding(t *testing.T) { require.Equal(t, uint32(0), resultTx.CheckTx.Code) require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + // query tx + txs = getTransactions(t, port, + fmt.Sprintf("action=begin-redelegation&delegator=%s", addr), + fmt.Sprintf("source-validator=%s", operAddrs[0]), + fmt.Sprintf("destination-validator=%s", operAddrs[1]), + ) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) + // query delegations, unbondings and redelegations from validator and delegator delegatorDels = getDelegatorDelegations(t, port, addr) require.Len(t, delegatorDels, 1) @@ -606,7 +614,7 @@ func TestBonding(t *testing.T) { // require.Equal(t, sdk.Unbonding, bondedValidators[0].Status) // query txs - txs := getBondingTxs(t, port, addr, "") + txs = getBondingTxs(t, port, addr, "") require.Len(t, txs, 3, "All Txs found") txs = getBondingTxs(t, port, addr, "bond") @@ -639,6 +647,11 @@ func TestSubmitProposal(t *testing.T) { // query proposal proposal := getProposal(t, port, proposalID) require.Equal(t, "Test", proposal.GetTitle()) + + // query tx + txs := getTransactions(t, port, fmt.Sprintf("action=submit-proposal&proposer=%s", addr)) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) } func TestDeposit(t *testing.T) { @@ -666,6 +679,11 @@ func TestDeposit(t *testing.T) { resultTx = doDeposit(t, port, seed, name, password, addr, proposalID, 5) tests.WaitForHeight(resultTx.Height+1, port) + // query tx + txs := getTransactions(t, port, fmt.Sprintf("action=deposit&depositor=%s", addr)) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) + // query proposal proposal = getProposal(t, port, proposalID) require.True(t, proposal.GetTotalDeposit().IsEqual(sdk.Coins{sdk.NewInt64Coin(stakeTypes.DefaultBondDenom, 10)})) @@ -708,6 +726,11 @@ func TestVote(t *testing.T) { resultTx = doVote(t, port, seed, name, password, addr, proposalID) tests.WaitForHeight(resultTx.Height+1, port) + // query tx + txs := getTransactions(t, port, fmt.Sprintf("action=vote&voter=%s", addr)) + require.Len(t, txs, 1) + require.Equal(t, resultTx.Height, txs[0].Height) + vote := getVote(t, port, proposalID, addr) require.Equal(t, proposalID, vote.ProposalID) require.Equal(t, gov.OptionYes, vote.Option) @@ -866,7 +889,7 @@ func TestProposalsQuery(t *testing.T) { //_____________________________________________________________________________ // get the account to get the sequence func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { - res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", addr), nil) + res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", addr.String()), nil) require.Equal(t, http.StatusOK, res.StatusCode, body) var acc auth.Account err := cdc.UnmarshalJSON([]byte(body), &acc) @@ -944,6 +967,22 @@ func doSend(t *testing.T, port, seed, name, password string, addr sdk.AccAddress return receiveAddr, resultTx } +func getTransactions(t *testing.T, port string, tags ...string) []tx.Info { + var txs []tx.Info + if len(tags) == 0 { + return txs + } + queryStr := strings.Join(tags, "&") + res, body := Request(t, port, "GET", fmt.Sprintf("/txs?%s", queryStr), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + err := cdc.UnmarshalJSON([]byte(body), &txs) + require.NoError(t, err) + return txs +} + +// ============= IBC Module ================ + func doIBCTransfer(t *testing.T, port, seed, name, password string, addr sdk.AccAddress) (resultTx ctypes.ResultBroadcastTxCommit) { // create receive address kb := client.MockKeyBase() @@ -984,6 +1023,8 @@ func doIBCTransfer(t *testing.T, port, seed, name, password string, addr sdk.Acc return resultTx } +// ============= Slashing Module ================ + func getSigningInfo(t *testing.T, port string, validatorPubKey string) slashing.ValidatorSigningInfo { res, body := Request(t, port, "GET", fmt.Sprintf("/slashing/validators/%s/signing_info", validatorPubKey), nil) require.Equal(t, http.StatusOK, res.StatusCode, body) @@ -995,6 +1036,31 @@ func getSigningInfo(t *testing.T, port string, validatorPubKey string) slashing. return signingInfo } +func doUnjail(t *testing.T, port, seed, name, password string, + valAddr sdk.ValAddress) (resultTx ctypes.ResultBroadcastTxCommit) { + chainID := viper.GetString(client.FlagChainID) + + jsonStr := []byte(fmt.Sprintf(`{ + "base_req": { + "name": "%s", + "password": "%s", + "chain_id": "%s", + "account_number":"1", + "sequence":"1" + } + }`, name, password, chainID)) + + res, body := Request(t, port, "POST", fmt.Sprintf("/slashing/validators/%s/unjail", valAddr.String()), jsonStr) + // TODO : fails with "401 must use own validator address" + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var results []ctypes.ResultBroadcastTxCommit + err := cdc.UnmarshalJSON([]byte(body), &results) + require.Nil(t, err) + + return results[0] +} + // ============= Stake Module ================ func getDelegation(t *testing.T, port string, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress) stake.Delegation { diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index 33dac880e..82e7c3429 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -209,14 +209,14 @@ paths: tags: - ICS0 summary: Search transactions - description: Search transactions by tag + description: Search transactions by tag(s). produces: - application/json parameters: - in: query name: tag type: string - description: "transaction tag, for instance: sender_bech32=`'cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc'`" + description: "transaction tags such as 'action=submit-proposal' and 'proposer=cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc' which results in the following endpoint: 'GET /txs?action=submit-proposal&proposer=cosmos1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc'" required: true - in: query name: page @@ -228,7 +228,7 @@ paths: type: integer responses: 200: - description: All Tx matching the provided tags + description: All txs matching the provided tags schema: type: array items: diff --git a/client/tx/search.go b/client/tx/search.go index 198bc4b33..422f48276 100644 --- a/client/tx/search.go +++ b/client/tx/search.go @@ -9,18 +9,18 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/cosmos/cosmos-sdk/client/utils" ctypes "github.com/tendermint/tendermint/rpc/core/types" ) const ( - flagTags = "tag" + flagTags = "tags" flagAny = "any" ) @@ -30,24 +30,35 @@ func SearchTxCmd(cdc *codec.Codec) *cobra.Command { Use: "txs", Short: "Search for all transactions that match the given tags.", Long: strings.TrimSpace(` -Search for transactions that match the given tags. By default, transactions must match ALL tags -passed to the --tags option. To match any transaction, use the --any option. +Search for transactions that match exactly the given tags. For example: -For example: - -$ gaiacli tendermint txs --tag test1,test2 - -will match any transaction tagged with both test1,test2. To match a transaction tagged with either -test1 or test2, use: - -$ gaiacli tendermint txs --tag test1,test2 --any +$ gaiacli query txs --tags ':&:' `), RunE: func(cmd *cobra.Command, args []string) error { - tags := viper.GetStringSlice(flagTags) + tagsStr := viper.GetString(flagTags) + tagsStr = strings.Trim(tagsStr, "'") + var tags []string + if strings.Contains(tagsStr, "&") { + tags = strings.Split(tagsStr, "&") + } else { + tags = append(tags, tagsStr) + } + + var tmTags []string + for _, tag := range tags { + if !strings.Contains(tag, ":") { + return fmt.Errorf("%s should be of the format :", tagsStr) + } else if strings.Count(tag, ":") > 1 { + return fmt.Errorf("%s should only contain one : pair", tagsStr) + } + + keyValue := strings.Split(tag, ":") + tag = fmt.Sprintf("%s='%s'", keyValue[0], keyValue[1]) + tmTags = append(tmTags, tag) + } cliCtx := context.NewCLIContext().WithCodec(cdc) - - txs, err := searchTxs(cliCtx, cdc, tags) + txs, err := searchTxs(cliCtx, cdc, tmTags) if err != nil { return err } @@ -74,8 +85,7 @@ $ gaiacli tendermint txs --tag test1,test2 --any viper.BindPFlag(client.FlagChainID, cmd.Flags().Lookup(client.FlagChainID)) cmd.Flags().Bool(client.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") viper.BindPFlag(client.FlagTrustNode, cmd.Flags().Lookup(client.FlagTrustNode)) - cmd.Flags().StringSlice(flagTags, nil, "Comma-separated list of tags that must match") - cmd.Flags().Bool(flagAny, false, "Return transactions that match ANY tag, rather than ALL") + cmd.Flags().String(flagTags, "", "tag:value list of tags that must match") return cmd } @@ -139,45 +149,35 @@ func FormatTxResults(cdc *codec.Codec, res []*ctypes.ResultTx) ([]Info, error) { // Search Tx REST Handler func SearchTxRequestHandlerFn(cliCtx context.CLIContext, cdc *codec.Codec) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - tag := r.FormValue("tag") - if tag == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You need to provide at least a tag as a key=value pair to search for. Postfix the key with _bech32 to search bech32-encoded addresses or public keys")) - return - } - - keyValue := strings.Split(tag, "=") - key := keyValue[0] - - value, err := url.QueryUnescape(keyValue[1]) + var tags []string + var txs []Info + err := r.ParseForm() if err != nil { - utils.WriteErrorResponse(w, http.StatusBadRequest, sdk.AppendMsgToErr("could not decode address", err.Error())) + utils.WriteErrorResponse(w, http.StatusBadRequest, sdk.AppendMsgToErr("could not parse query parameters", err.Error())) + return + } + if len(r.Form) == 0 { + utils.PostProcessResponse(w, cdc, txs, cliCtx.Indent) return } - if strings.HasSuffix(key, "_bech32") { - bech32address := strings.Trim(value, "'") - prefix := strings.Split(bech32address, "1")[0] - bz, err := sdk.GetFromBech32(bech32address, prefix) + for key, values := range r.Form { + value, err := url.QueryUnescape(values[0]) if err != nil { - utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(w, http.StatusBadRequest, sdk.AppendMsgToErr("could not decode query value", err.Error())) return } - tag = strings.TrimRight(key, "_bech32") + "='" + sdk.AccAddress(bz).String() + "'" + tag := fmt.Sprintf("%s='%s'", key, value) + tags = append(tags, tag) } - txs, err := searchTxs(cliCtx, cdc, []string{tag}) + txs, err = searchTxs(cliCtx, cdc, tags) if err != nil { utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if len(txs) == 0 { - w.Write([]byte("[]")) - return - } - utils.PostProcessResponse(w, cdc, txs, cliCtx.Indent) } } diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index f2c2e4ba1..b4f7ee98d 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -21,6 +21,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/server" @@ -277,7 +278,7 @@ func TestGaiaCLICreateValidator(t *testing.T) { require.NotZero(t, validatorDelegations[0].Shares) // unbond a single share - unbondStr := fmt.Sprintf("gaiacli tx stake unbond begin %v", flags) + unbondStr := fmt.Sprintf("gaiacli tx stake unbond %v", flags) unbondStr += fmt.Sprintf(" --from=%s", "bar") unbondStr += fmt.Sprintf(" --validator=%s", sdk.ValAddress(barAddr)) unbondStr += fmt.Sprintf(" --shares-amount=%v", "1") @@ -354,6 +355,9 @@ func TestGaiaCLISubmitProposal(t *testing.T) { executeWrite(t, spStr, app.DefaultKeyPass) tests.WaitForNextNBlocksTM(2, port) + txs := executeGetTxs(t, fmt.Sprintf("gaiacli query txs --tags='action:submit-proposal&proposer:%s' %v", fooAddr, flags)) + require.Len(t, txs, 1) + fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli query account %s %v", fooAddr, flags)) require.Equal(t, int64(45), fooAcc.GetCoins().AmountOf(stakeTypes.DefaultBondDenom).Int64()) @@ -398,6 +402,9 @@ func TestGaiaCLISubmitProposal(t *testing.T) { fooAddr, flags)) require.Equal(t, int64(15), deposit.Amount.AmountOf(stakeTypes.DefaultBondDenom).Int64()) + txs = executeGetTxs(t, fmt.Sprintf("gaiacli query txs --tags=action:deposit&depositor:%s %v", fooAddr, flags)) + require.Len(t, txs, 1) + fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli query account %s %v", fooAddr, flags)) require.Equal(t, int64(35), fooAcc.GetCoins().AmountOf(stakeTypes.DefaultBondDenom).Int64()) @@ -432,6 +439,9 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, uint64(1), votes[0].ProposalID) require.Equal(t, gov.OptionYes, votes[0].Option) + txs = executeGetTxs(t, fmt.Sprintf("gaiacli query txs --tags=action:vote&voter:%s %v", fooAddr, flags)) + require.Len(t, txs, 1) + proposalsQuery, _ = tests.ExecuteT(t, fmt.Sprintf("gaiacli query gov proposals --status=DepositPeriod %v", flags), "") require.Equal(t, "No matching proposals found", proposalsQuery) @@ -736,6 +746,18 @@ func executeGetAccount(t *testing.T, cmdStr string) auth.BaseAccount { return acc } +//___________________________________________________________________________________ +// txs + +func executeGetTxs(t *testing.T, cmdStr string) []tx.Info { + out, _ := tests.ExecuteT(t, cmdStr, "") + var txs []tx.Info + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &txs) + require.NoError(t, err, "out %v\n, err %v", out, err) + return txs +} + //___________________________________________________________________________________ // stake diff --git a/docs/gaia/gaiacli.md b/docs/gaia/gaiacli.md index 38eaa0b8b..5c10e4898 100644 --- a/docs/gaia/gaiacli.md +++ b/docs/gaia/gaiacli.md @@ -183,10 +183,50 @@ gaiacli tx sign --validate-signatures signedSendTx.json You can broadcast the signed transaction to a node by providing the JSON file to the following command: -``` +```bash gaiacli tx broadcast --node= signedSendTx.json ``` +### Query Transactions + +#### Matching a set of tags + +You can use the transaction search command to query for transactions that match a specific set of `tags`, which are added on every transaction. + +Each tag is conformed by a key-value pair in the form of `:`. Tags can also be combined to query for a more specific result using the `&` symbol. + +The command for querying transactions using a `tag` is the following: + +```bash +gaiacli query txs --tags=':' +``` + +And for using multiple `tags`: + +```bash +gaiacli query txs --tags=':&:' +``` + +::: tip Note + +You can find a list of available `tags` on each of the SDK modules: + +- [Common tags](https://github.com/cosmos/cosmos-sdk/blob/d1e76221d8e28824bb4791cb4ad8662d2ae9051e/types/tags.go#L57-L63) +- [Staking tags](https://github.com/cosmos/cosmos-sdk/blob/d1e76221d8e28824bb4791cb4ad8662d2ae9051e/x/stake/tags/tags.go#L8-L24) +- [Governance tags](https://github.com/cosmos/cosmos-sdk/blob/d1e76221d8e28824bb4791cb4ad8662d2ae9051e/x/gov/tags/tags.go#L8-L22) +- [Slashing tags](https://github.com/cosmos/cosmos-sdk/blob/d1e76221d8e28824bb4791cb4ad8662d2ae9051e/x/slashing/handler.go#L52) +- [Distribution tags](https://github.com/cosmos/cosmos-sdk/blob/develop/x/distribution/tags/tags.go#L8-L17) +- [Bank tags](https://github.com/cosmos/cosmos-sdk/blob/d1e76221d8e28824bb4791cb4ad8662d2ae9051e/x/bank/keeper.go#L193-L206) +::: + +#### Matching a transaction's hash + +You can also query a single transaction by its hash using the following command: + +```bash +gaiacli query tx [hash] +``` + ### Staking #### Set up a Validator