diff --git a/server/grpc/server_test.go b/server/grpc/server_test.go index 5750b9e2e2b7..7a58bef00dbe 100644 --- a/server/grpc/server_test.go +++ b/server/grpc/server_test.go @@ -121,7 +121,7 @@ func (s *IntegrationTestSuite) TestGRPCServer_GetTxsEvent() { _, err := txServiceClient.GetTxsEvent( context.Background(), &tx.GetTxsEventRequest{ - Events: []string{"message.action=send"}, + Events: []string{"message.action='send'"}, }, ) s.Require().NoError(err) diff --git a/x/auth/tx/service.go b/x/auth/tx/service.go index 853d401beccc..a80fb3e0040d 100644 --- a/x/auth/tx/service.go +++ b/x/auth/tx/service.go @@ -2,7 +2,6 @@ package tx import ( "context" - "encoding/hex" "fmt" "strings" @@ -11,11 +10,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - abci "github.com/tendermint/tendermint/abci/types" - tmtypes "github.com/tendermint/tendermint/types" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" pagination "github.com/cosmos/cosmos-sdk/types/query" @@ -62,57 +57,36 @@ func (s txServer) GetTxsEvent(ctx context.Context, req *txtypes.GetTxsEventReque return nil, status.Error(codes.InvalidArgument, "must declare at least one event to search") } - tmEvents := make([]string, len(req.Events)) - for i, event := range req.Events { - if !strings.Contains(event, "=") { - return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid event; event %s should be of the format: %s", event, eventFormat)) - } else if strings.Count(event, "=") > 1 { + for _, event := range req.Events { + if strings.Count(event, "=") != 1 { return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid event; event %s should be of the format: %s", event, eventFormat)) } - - tokens := strings.Split(event, "=") - if tokens[0] == tmtypes.TxHeightKey { - event = fmt.Sprintf("%s=%s", tokens[0], tokens[1]) - } else { - event = fmt.Sprintf("%s='%s'", tokens[0], tokens[1]) - } - - tmEvents[i] = event } - query := strings.Join(tmEvents, " AND ") - - result, err := s.clientCtx.Client.TxSearch(ctx, query, false, &page, &limit, "") + result, err := queryTxsByEvents(ctx, s.clientCtx, req.Events, page, limit, "") if err != nil { return nil, err } // Create a proto codec, we need it to unmarshal the tx bytes. - cdc := codec.NewProtoCodec(s.clientCtx.InterfaceRegistry) - txRespList := make([]*sdk.TxResponse, len(result.Txs)) txsList := make([]*txtypes.Tx, len(result.Txs)) for i, tx := range result.Txs { - txResp := txResultToTxResponse(&tx.TxResult) - txResp.Height = tx.Height - txResp.TxHash = tx.Hash.String() - txRespList[i] = txResp - - var protoTx txtypes.Tx - if err := cdc.UnmarshalBinaryBare(tx.Tx, &protoTx); err != nil { - return nil, err + protoTx, ok := tx.Tx.GetCachedValue().(*txtypes.Tx) + if !ok { + return nil, status.Errorf(codes.Internal, "expected %T, got %T", txtypes.Tx{}, tx.Tx.GetCachedValue()) } - txsList[i] = &protoTx + + txsList[i] = protoTx } return &txtypes.GetTxsEventResponse{ Txs: txsList, - TxResponses: txRespList, + TxResponses: result.Txs, Pagination: &pagination.PageResponse{ - Total: uint64(result.TotalCount), + Total: result.TotalCount, }, }, nil - } // Simulate implements the ServiceServer.Simulate RPC method. @@ -147,34 +121,21 @@ func (s txServer) GetTx(ctx context.Context, req *txtypes.GetTxRequest) (*txtype return nil, status.Error(codes.InvalidArgument, "request cannot be nil") } - // We get hash as a hex string in the request, convert it to bytes. - hash, err := hex.DecodeString(req.Hash) - if err != nil { - return nil, err - } - // TODO We should also check the proof flag in gRPC header. // https://github.com/cosmos/cosmos-sdk/issues/7036. - result, err := s.clientCtx.Client.Tx(ctx, hash, false) + result, err := queryTx(ctx, s.clientCtx, req.Hash) if err != nil { return nil, err } - // Create a proto codec, we need it to unmarshal the tx bytes. - cdc := codec.NewProtoCodec(s.clientCtx.InterfaceRegistry) - - var protoTx txtypes.Tx - if err := cdc.UnmarshalBinaryBare(result.Tx, &protoTx); err != nil { - return nil, err + protoTx, ok := result.Tx.GetCachedValue().(*txtypes.Tx) + if !ok { + return nil, status.Errorf(codes.Internal, "expected %T, got %T", txtypes.Tx{}, result.Tx.GetCachedValue()) } - txResp := txResultToTxResponse(&result.TxResult) - txResp.Height = result.Height - txResp.TxHash = result.Hash.String() - return &txtypes.GetTxResponse{ - Tx: &protoTx, - TxResponse: txResp, + Tx: protoTx, + TxResponse: result, }, nil } @@ -200,15 +161,3 @@ func RegisterTxService( func RegisterGRPCGatewayRoutes(clientConn gogogrpc.ClientConn, mux *runtime.ServeMux) { txtypes.RegisterServiceHandlerClient(context.Background(), mux, txtypes.NewServiceClient(clientConn)) } - -func txResultToTxResponse(respTx *abci.ResponseDeliverTx) *sdk.TxResponse { - logs, _ := sdk.ParseABCILogs(respTx.Log) - return &sdk.TxResponse{ - Code: respTx.Code, - Codespace: respTx.Codespace, - GasUsed: respTx.GasUsed, - GasWanted: respTx.GasWanted, - Info: respTx.Info, - Logs: logs, - } -} diff --git a/x/auth/tx/service_test.go b/x/auth/tx/service_test.go index b7b3322dc33f..b8cb4883dc74 100644 --- a/x/auth/tx/service_test.go +++ b/x/auth/tx/service_test.go @@ -14,7 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - query "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/types/tx/signing" @@ -175,14 +175,14 @@ func (s IntegrationTestSuite) TestGetTxEvents_GRPC() { { "without pagination", &tx.GetTxsEventRequest{ - Events: []string{"message.action=send"}, + Events: []string{"message.action='send'"}, }, false, "", }, { "with pagination", &tx.GetTxsEventRequest{ - Events: []string{"message.action=send"}, + Events: []string{"message.action='send'"}, Pagination: &query.PageRequest{ CountTotal: false, Offset: 0, @@ -194,7 +194,7 @@ func (s IntegrationTestSuite) TestGetTxEvents_GRPC() { { "with multi events", &tx.GetTxsEventRequest{ - Events: []string{"message.action=send", "message.module=bank"}, + Events: []string{"message.action='send'", "message.module='bank'"}, }, false, "", }, @@ -210,6 +210,12 @@ func (s IntegrationTestSuite) TestGetTxEvents_GRPC() { s.Require().NoError(err) s.Require().GreaterOrEqual(len(grpcRes.Txs), 1) s.Require().Equal("foobar", grpcRes.Txs[0].Body.Memo) + + // Make sure fields are populated. + // ref: https://github.com/cosmos/cosmos-sdk/issues/8680 + // ref: https://github.com/cosmos/cosmos-sdk/issues/8681 + s.Require().NotEmpty(grpcRes.TxResponses[0].Timestamp) + s.Require().NotEmpty(grpcRes.TxResponses[0].RawLog) } }) } @@ -231,25 +237,25 @@ func (s IntegrationTestSuite) TestGetTxEvents_GRPCGateway() { }, { "without pagination", - fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s", val.APIAddress, "message.action=send"), + fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s", val.APIAddress, "message.action='send'"), false, "", }, { "with pagination", - fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s&pagination.offset=%d&pagination.limit=%d", val.APIAddress, "message.action=send", 0, 10), + fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s&pagination.offset=%d&pagination.limit=%d", val.APIAddress, "message.action='send'", 0, 10), false, "", }, { "expect pass with multiple-events", - fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s&events=%s", val.APIAddress, "message.action=send", "message.module=bank"), + fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s&events=%s", val.APIAddress, "message.action='send'", "message.module='bank'"), false, "", }, { "expect pass with escape event", - fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s", val.APIAddress, "message.action%3Dsend"), + fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s", val.APIAddress, "message.action%3D'send'"), false, "", }, @@ -335,6 +341,12 @@ func (s IntegrationTestSuite) TestGetTx_GRPCGateway() { s.Require().NoError(err) s.Require().Equal("foobar", result.Tx.Body.Memo) s.Require().NotZero(result.TxResponse.Height) + + // Make sure fields are populated. + // ref: https://github.com/cosmos/cosmos-sdk/issues/8680 + // ref: https://github.com/cosmos/cosmos-sdk/issues/8681 + s.Require().NotEmpty(result.TxResponse.Timestamp) + s.Require().NotEmpty(result.TxResponse.RawLog) } }) } diff --git a/x/auth/tx/xauthclient.go b/x/auth/tx/xauthclient.go new file mode 100644 index 000000000000..057afe529d8f --- /dev/null +++ b/x/auth/tx/xauthclient.go @@ -0,0 +1,157 @@ +// Package tx 's xauthclient.go file is copy-pasted from +// https://github.com/cosmos/cosmos-sdk/blob/v0.41.3/x/auth/client/query.go +// It is duplicated as to not introduce any breaking change in 0.41.4, see PR: +// https://github.com/cosmos/cosmos-sdk/pull/8732#discussion_r584746947 +package tx + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + + ctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// QueryTxsByEvents performs a search for transactions for a given set of events +// via the Tendermint RPC. An event takes the form of: +// "{eventAttribute}.{attributeKey} = '{attributeValue}'". Each event is +// concatenated with an 'AND' operand. It returns a slice of Info object +// containing txs and metadata. An error is returned if the query fails. +// If an empty string is provided it will order txs by asc +func queryTxsByEvents(goCtx context.Context, clientCtx client.Context, events []string, page, limit int, orderBy string) (*sdk.SearchTxsResult, error) { + if len(events) == 0 { + return nil, errors.New("must declare at least one event to search") + } + + if page <= 0 { + return nil, errors.New("page must greater than 0") + } + + if limit <= 0 { + return nil, errors.New("limit must greater than 0") + } + + // XXX: implement ANY + query := strings.Join(events, " AND ") + + node, err := clientCtx.GetNode() + if err != nil { + return nil, err + } + + // TODO: this may not always need to be proven + // https://github.com/cosmos/cosmos-sdk/issues/6807 + resTxs, err := node.TxSearch(goCtx, query, true, &page, &limit, orderBy) + if err != nil { + return nil, err + } + + resBlocks, err := getBlocksForTxResults(goCtx, clientCtx, resTxs.Txs) + if err != nil { + return nil, err + } + + txs, err := formatTxResults(clientCtx.TxConfig, resTxs.Txs, resBlocks) + if err != nil { + return nil, err + } + + result := sdk.NewSearchTxsResult(uint64(resTxs.TotalCount), uint64(len(txs)), uint64(page), uint64(limit), txs) + + return result, nil +} + +// QueryTx queries for a single transaction by a hash string in hex format. An +// error is returned if the transaction does not exist or cannot be queried. +func queryTx(goCtx context.Context, clientCtx client.Context, hashHexStr string) (*sdk.TxResponse, error) { + hash, err := hex.DecodeString(hashHexStr) + if err != nil { + return nil, err + } + + node, err := clientCtx.GetNode() + if err != nil { + return nil, err + } + + //TODO: this may not always need to be proven + // https://github.com/cosmos/cosmos-sdk/issues/6807 + resTx, err := node.Tx(goCtx, hash, true) + if err != nil { + return nil, err + } + + resBlocks, err := getBlocksForTxResults(goCtx, clientCtx, []*ctypes.ResultTx{resTx}) + if err != nil { + return nil, err + } + + out, err := mkTxResult(clientCtx.TxConfig, resTx, resBlocks[resTx.Height]) + if err != nil { + return out, err + } + + return out, nil +} + +// formatTxResults parses the indexed txs into a slice of TxResponse objects. +func formatTxResults(txConfig client.TxConfig, resTxs []*ctypes.ResultTx, resBlocks map[int64]*ctypes.ResultBlock) ([]*sdk.TxResponse, error) { + var err error + out := make([]*sdk.TxResponse, len(resTxs)) + for i := range resTxs { + out[i], err = mkTxResult(txConfig, resTxs[i], resBlocks[resTxs[i].Height]) + if err != nil { + return nil, err + } + } + + return out, nil +} + +func getBlocksForTxResults(goCtx context.Context, clientCtx client.Context, resTxs []*ctypes.ResultTx) (map[int64]*ctypes.ResultBlock, error) { + node, err := clientCtx.GetNode() + if err != nil { + return nil, err + } + + resBlocks := make(map[int64]*ctypes.ResultBlock) + + for _, resTx := range resTxs { + if _, ok := resBlocks[resTx.Height]; !ok { + resBlock, err := node.Block(goCtx, &resTx.Height) + if err != nil { + return nil, err + } + + resBlocks[resTx.Height] = resBlock + } + } + + return resBlocks, nil +} + +func mkTxResult(txConfig client.TxConfig, resTx *ctypes.ResultTx, resBlock *ctypes.ResultBlock) (*sdk.TxResponse, error) { + txb, err := txConfig.TxDecoder()(resTx.Tx) + if err != nil { + return nil, err + } + p, ok := txb.(intoAny) + if !ok { + return nil, fmt.Errorf("expecting a type implementing intoAny, got: %T", txb) + } + any := p.AsAny() + return sdk.NewResponseResultTx(resTx, any, resBlock.Block.Time.Format(time.RFC3339)), nil +} + +// Deprecated: this interface is used only internally for scenario we are +// deprecating (StdTxConfig support) +type intoAny interface { + AsAny() *codectypes.Any +}