* feat: Query txs by signature and by address+seq (#9750)
<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->
## Description
Closes: #9741
<!-- Add a description of the changes that this PR introduces and the files that
are the most critical to review. -->
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
(cherry picked from commit 7c19434000
)
# Conflicts:
# CHANGELOG.md
# x/auth/client/cli/query.go
* Fix conflicts
* Fix cl
* Fix conflicts
Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com>
This commit is contained in:
parent
b402d1ce37
commit
e7c5f6b1fe
|
@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Features
|
||||
|
||||
* [\#9750](https://github.com/cosmos/cosmos-sdk/pull/9750) Emit events for tx signature and sequence, so clients can now query txs by signature (`tx.signature='<base64_sig>'`) or by address and sequence combo (`tx.acc_seq='<addr>/<seq>'`).
|
||||
|
||||
### Improvements
|
||||
|
||||
* (cli) [\#9717](https://github.com/cosmos/cosmos-sdk/pull/9717) Added CLI flag `--output json/text` to `tx` cli commands.
|
||||
|
|
|
@ -223,6 +223,11 @@ func toBytes(i interface{}) []byte {
|
|||
|
||||
// Common event types and attribute keys
|
||||
var (
|
||||
EventTypeTx = "tx"
|
||||
|
||||
AttributeKeyAccountSequence = "acc_seq"
|
||||
AttributeKeySignature = "signature"
|
||||
|
||||
EventTypeMessage = "message"
|
||||
|
||||
AttributeKeyAction = "action"
|
||||
|
|
|
@ -2,6 +2,7 @@ package ante
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
|
@ -94,6 +95,34 @@ func (spkd SetPubKeyDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
|
|||
spkd.ak.SetAccount(ctx, acc)
|
||||
}
|
||||
|
||||
// Also emit the following events, so that txs can be indexed by these
|
||||
// indices:
|
||||
// - signature (via `tx.signature='<sig_as_base64>'`),
|
||||
// - concat(address,"/",sequence) (via `tx.acc_seq='cosmos1abc...def/42'`).
|
||||
sigs, err := sigTx.GetSignaturesV2()
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
var events sdk.Events
|
||||
for i, sig := range sigs {
|
||||
events = append(events, sdk.NewEvent(sdk.EventTypeTx,
|
||||
sdk.NewAttribute(sdk.AttributeKeyAccountSequence, fmt.Sprintf("%s/%d", signers[i], sig.Sequence)),
|
||||
))
|
||||
|
||||
sigBzs, err := signatureDataToBz(sig.Data)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
for _, sigBz := range sigBzs {
|
||||
events = append(events, sdk.NewEvent(sdk.EventTypeTx,
|
||||
sdk.NewAttribute(sdk.AttributeKeySignature, base64.StdEncoding.EncodeToString(sigBz)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvents(events)
|
||||
|
||||
return next(ctx, tx, simulate)
|
||||
}
|
||||
|
||||
|
@ -447,3 +476,42 @@ func CountSubKeys(pub cryptotypes.PubKey) int {
|
|||
|
||||
return numKeys
|
||||
}
|
||||
|
||||
// signatureDataToBz converts a SignatureData into raw bytes signature.
|
||||
// For SingleSignatureData, it returns the signature raw bytes.
|
||||
// For MultiSignatureData, it returns an array of all individual signatures,
|
||||
// as well as the aggregated signature.
|
||||
func signatureDataToBz(data signing.SignatureData) ([][]byte, error) {
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("got empty SignatureData")
|
||||
}
|
||||
|
||||
switch data := data.(type) {
|
||||
case *signing.SingleSignatureData:
|
||||
return [][]byte{data.Signature}, nil
|
||||
case *signing.MultiSignatureData:
|
||||
sigs := [][]byte{}
|
||||
var err error
|
||||
|
||||
for _, d := range data.Signatures {
|
||||
nestedSigs, err := signatureDataToBz(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigs = append(sigs, nestedSigs...)
|
||||
}
|
||||
|
||||
multisig := cryptotypes.MultiSignature{
|
||||
Signatures: sigs,
|
||||
}
|
||||
aggregatedSig, err := multisig.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigs = append(sigs, aggregatedSig)
|
||||
|
||||
return sigs, nil
|
||||
default:
|
||||
return nil, sdkerrors.ErrInvalidType.Wrapf("unexpected signature data type %T", data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
|
@ -18,6 +20,11 @@ import (
|
|||
|
||||
const (
|
||||
flagEvents = "events"
|
||||
flagType = "type"
|
||||
|
||||
typeHash = "hash"
|
||||
typeAccSeq = "acc_seq"
|
||||
typeSig = "signature"
|
||||
|
||||
eventFormat = "{eventType}.{eventAttribute}={value}"
|
||||
)
|
||||
|
@ -210,14 +217,34 @@ $ %s query txs --%s 'message.sender=cosmos1...&message.action=withdraw_delegator
|
|||
// QueryTxCmd implements the default command for a tx query.
|
||||
func QueryTxCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "tx [hash]",
|
||||
Short: "Query for a transaction by hash in a committed block",
|
||||
Use: "tx --type=[hash|acc_seq|signature] [hash|acc_seq|signature]",
|
||||
Short: "Query for a transaction by hash, addr++seq combination or signature in a committed block",
|
||||
Long: strings.TrimSpace(fmt.Sprintf(`
|
||||
Example:
|
||||
$ %s query tx <hash>
|
||||
$ %s query tx --%s=%s <addr>:<sequence>
|
||||
$ %s query tx --%s=%s <sig1_base64,sig2_base64...>
|
||||
`,
|
||||
version.AppName,
|
||||
version.AppName, flagType, typeAccSeq,
|
||||
version.AppName, flagType, typeSig)),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientQueryContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typ, _ := cmd.Flags().GetString(flagType)
|
||||
|
||||
switch typ {
|
||||
case typeHash:
|
||||
{
|
||||
if args[0] == "" {
|
||||
return fmt.Errorf("argument should be a tx hash")
|
||||
}
|
||||
|
||||
// If hash is given, then query the tx by hash.
|
||||
output, err := authtx.QueryTx(clientCtx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -228,10 +255,72 @@ func QueryTxCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
return clientCtx.PrintProto(output)
|
||||
}
|
||||
case typeSig:
|
||||
{
|
||||
sigParts, err := parseSigArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmEvents := make([]string, len(sigParts))
|
||||
for i, sig := range sigParts {
|
||||
tmEvents[i] = fmt.Sprintf("%s.%s='%s'", sdk.EventTypeTx, sdk.AttributeKeySignature, sig)
|
||||
}
|
||||
|
||||
txs, err := authtx.QueryTxsByEvents(clientCtx, tmEvents, rest.DefaultPage, query.DefaultLimit, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(txs.Txs) == 0 {
|
||||
return fmt.Errorf("found no txs matching given signatures")
|
||||
}
|
||||
if len(txs.Txs) > 1 {
|
||||
// This case means there's a bug somewhere else in the code. Should not happen.
|
||||
return errors.ErrLogic.Wrapf("found %d txs matching given signatures", len(txs.Txs))
|
||||
}
|
||||
|
||||
return clientCtx.PrintProto(txs.Txs[0])
|
||||
}
|
||||
case typeAccSeq:
|
||||
{
|
||||
if args[0] == "" {
|
||||
return fmt.Errorf("`acc_seq` type takes an argument '<addr>/<seq>'")
|
||||
}
|
||||
|
||||
tmEvents := []string{
|
||||
fmt.Sprintf("%s.%s='%s'", sdk.EventTypeTx, sdk.AttributeKeyAccountSequence, args[0]),
|
||||
}
|
||||
txs, err := authtx.QueryTxsByEvents(clientCtx, tmEvents, rest.DefaultPage, query.DefaultLimit, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(txs.Txs) == 0 {
|
||||
return fmt.Errorf("found no txs matching given address and sequence combination")
|
||||
}
|
||||
if len(txs.Txs) > 1 {
|
||||
// This case means there's a bug somewhere else in the code. Should not happen.
|
||||
return fmt.Errorf("found %d txs matching given address and sequence combination", len(txs.Txs))
|
||||
}
|
||||
|
||||
return clientCtx.PrintProto(txs.Txs[0])
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown --%s value %s", flagType, typ)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags.AddQueryFlagsToCmd(cmd)
|
||||
cmd.Flags().String(flagType, typeHash, fmt.Sprintf("The type to be used when querying tx, can be one of \"%s\", \"%s\", \"%s\"", typeHash, typeAccSeq, typeSig))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// parseSigArgs parses comma-separated signatures from the CLI arguments.
|
||||
func parseSigArgs(args []string) ([]string, error) {
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return nil, fmt.Errorf("argument should be comma-separated signatures")
|
||||
}
|
||||
|
||||
return strings.Split(args[0], ","), nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseSigs(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expErr bool
|
||||
expNumSigs int
|
||||
}{
|
||||
{"no args", []string{}, true, 0},
|
||||
{"empty args", []string{""}, true, 0},
|
||||
{"too many args", []string{"foo", "bar"}, true, 0},
|
||||
{"1 sig", []string{"foo"}, false, 1},
|
||||
{"3 sigs", []string{"foo,bar,baz"}, false, 3},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
sigs, err := parseSigArgs(tc.args)
|
||||
if tc.expErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expNumSigs, len(sigs))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package testutil
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -243,7 +244,7 @@ func checkSignatures(require *require.Assertions, txCfg client.TxConfig, output
|
|||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestCLIQueryTxCmd() {
|
||||
func (s *IntegrationTestSuite) TestCLIQueryTxCmdByHash() {
|
||||
val := s.network.Validators[0]
|
||||
|
||||
account2, err := val.ClientCtx.Keyring.Key("newAccount2")
|
||||
|
@ -267,23 +268,26 @@ func (s *IntegrationTestSuite) TestCLIQueryTxCmd() {
|
|||
expectErr bool
|
||||
rawLogContains string
|
||||
}{
|
||||
{
|
||||
"not enough args",
|
||||
[]string{},
|
||||
true, "",
|
||||
},
|
||||
{
|
||||
"with invalid hash",
|
||||
[]string{"somethinginvalid", fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
|
||||
true,
|
||||
"",
|
||||
true, "",
|
||||
},
|
||||
{
|
||||
"with valid and not existing hash",
|
||||
[]string{"C7E7D3A86A17AB3A321172239F3B61357937AF0F25D9FA4D2F4DCCAD9B0D7747", fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
|
||||
true,
|
||||
"",
|
||||
true, "",
|
||||
},
|
||||
{
|
||||
"happy case",
|
||||
[]string{txRes.TxHash, fmt.Sprintf("--%s=json", tmcli.OutputFlag)},
|
||||
false,
|
||||
"/cosmos.bank.v1beta1.MsgSend",
|
||||
sdk.MsgTypeURL(&banktypes.MsgSend{}),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -308,6 +312,120 @@ func (s *IntegrationTestSuite) TestCLIQueryTxCmd() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestCLIQueryTxCmdByEvents() {
|
||||
val := s.network.Validators[0]
|
||||
|
||||
account2, err := val.ClientCtx.Keyring.Key("newAccount2")
|
||||
s.Require().NoError(err)
|
||||
|
||||
sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10)
|
||||
|
||||
// Send coins.
|
||||
out, err := s.createBankMsg(
|
||||
val, account2.GetAddress(),
|
||||
sdk.NewCoins(sendTokens),
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
var txRes sdk.TxResponse
|
||||
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txRes))
|
||||
s.Require().NoError(s.network.WaitForNextBlock())
|
||||
|
||||
// Query the tx by hash to get the inner tx.
|
||||
out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, authcli.QueryTxCmd(), []string{txRes.TxHash, fmt.Sprintf("--%s=json", tmcli.OutputFlag)})
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txRes))
|
||||
protoTx := txRes.GetTx().(*tx.Tx)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectErr bool
|
||||
expectErrStr string
|
||||
}{
|
||||
{
|
||||
"invalid --type",
|
||||
[]string{
|
||||
fmt.Sprintf("--type=%s", "foo"),
|
||||
"bar",
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
true, "unknown --type value foo",
|
||||
},
|
||||
{
|
||||
"--type=acc_seq with no addr+seq",
|
||||
[]string{
|
||||
"--type=acc_seq",
|
||||
"",
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
true, "`acc_seq` type takes an argument '<addr>/<seq>'",
|
||||
},
|
||||
{
|
||||
"non-existing addr+seq combo",
|
||||
[]string{
|
||||
"--type=acc_seq",
|
||||
"foobar",
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
true, "found no txs matching given address and sequence combination",
|
||||
},
|
||||
{
|
||||
"addr+seq happy case",
|
||||
[]string{
|
||||
"--type=acc_seq",
|
||||
fmt.Sprintf("%s/%d", val.Address, protoTx.AuthInfo.SignerInfos[0].Sequence),
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
false, "",
|
||||
},
|
||||
{
|
||||
"--type=signature with no signature",
|
||||
[]string{
|
||||
"--type=signature",
|
||||
"",
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
true, "argument should be comma-separated signatures",
|
||||
},
|
||||
{
|
||||
"non-existing signatures",
|
||||
[]string{
|
||||
"--type=signature",
|
||||
"foo",
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
true, "found no txs matching given signatures",
|
||||
},
|
||||
{
|
||||
"with --signatures happy case",
|
||||
[]string{
|
||||
"--type=signature",
|
||||
base64.StdEncoding.EncodeToString(protoTx.Signatures[0]),
|
||||
fmt.Sprintf("--%s=json", tmcli.OutputFlag),
|
||||
},
|
||||
false, "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
s.Run(tc.name, func() {
|
||||
cmd := authcli.QueryTxCmd()
|
||||
clientCtx := val.ClientCtx
|
||||
|
||||
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
|
||||
if tc.expectErr {
|
||||
s.Require().Error(err)
|
||||
s.Require().Contains(err.Error(), tc.expectErrStr)
|
||||
} else {
|
||||
var result sdk.TxResponse
|
||||
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &result))
|
||||
s.Require().NotNil(result.Height)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TestCLISendGenerateSignAndBroadcast() {
|
||||
val1 := s.network.Validators[0]
|
||||
|
||||
|
@ -679,7 +797,7 @@ func (s *IntegrationTestSuite) TestCLIMultisign() {
|
|||
|
||||
sign1File := testutil.WriteToNewTempFile(s.T(), account1Signature.String())
|
||||
|
||||
// Sign with account1
|
||||
// Sign with account2
|
||||
account2Signature, err := TxSignExec(val1.ClientCtx, account2.GetAddress(), multiGeneratedTxFile.Name(), "--multisig", multisigInfo.GetAddress().String())
|
||||
s.Require().NoError(err)
|
||||
|
||||
|
|
Loading…
Reference in New Issue