cosmos-sdk/x/auth/tx/service_test.go

479 lines
14 KiB
Go

package tx_test
import (
"context"
"fmt"
"testing"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
"github.com/stretchr/testify/suite"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
clienttx "github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "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/types/tx"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
type IntegrationTestSuite struct {
suite.Suite
cfg network.Config
network *network.Network
queryClient tx.ServiceClient
txRes sdk.TxResponse
}
func (s *IntegrationTestSuite) SetupSuite() {
s.T().Log("setting up integration test suite")
cfg := network.DefaultConfig()
cfg.NumValidators = 1
s.cfg = cfg
s.network = network.New(s.T(), cfg)
s.Require().NotNil(s.network)
val := s.network.Validators[0]
_, err := s.network.WaitForHeight(1)
s.Require().NoError(err)
s.queryClient = tx.NewServiceClient(val.ClientCtx)
// Create a new MsgSend tx from val to itself.
out, err := bankcli.MsgSendExec(
val.ClientCtx,
val.Address,
val.Address,
sdk.NewCoins(
sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10)),
),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
fmt.Sprintf("--gas=%d", flags.DefaultGasLimit),
fmt.Sprintf("--%s=foobar", flags.FlagMemo),
)
s.Require().NoError(err)
s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &s.txRes))
s.Require().Equal(uint32(0), s.txRes.Code)
s.Require().NoError(s.network.WaitForNextBlock())
}
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
func (s IntegrationTestSuite) TestSimulateTx_GRPC() {
txBuilder := s.mkTxBuilder()
// Convert the txBuilder to a tx.Tx.
protoTx, err := txBuilderToProtoTx(txBuilder)
s.Require().NoError(err)
testCases := []struct {
name string
req *tx.SimulateRequest
expErr bool
expErrMsg string
}{
{"nil request", nil, true, "request cannot be nil"},
{"empty request", &tx.SimulateRequest{}, true, "invalid empty tx"},
{"valid request", &tx.SimulateRequest{Tx: protoTx}, false, ""},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
// Broadcast the tx via gRPC via the validator's clientCtx (which goes
// through Tendermint).
res, err := s.queryClient.Simulate(context.Background(), tc.req)
if tc.expErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expErrMsg)
} else {
s.Require().NoError(err)
// Check the result and gas used are correct.
s.Require().Equal(len(res.GetResult().GetEvents()), 6) // 1 coin recv 1 coin spent, 1 transfer, 3 messages.
s.Require().True(res.GetGasInfo().GetGasUsed() > 0) // Gas used sometimes change, just check it's not empty.
}
})
}
}
func (s IntegrationTestSuite) TestSimulateTx_GRPCGateway() {
val := s.network.Validators[0]
txBuilder := s.mkTxBuilder()
// Convert the txBuilder to a tx.Tx.
protoTx, err := txBuilderToProtoTx(txBuilder)
s.Require().NoError(err)
testCases := []struct {
name string
req *tx.SimulateRequest
expErr bool
expErrMsg string
}{
{"empty request", &tx.SimulateRequest{}, true, "invalid empty tx"},
{"valid request", &tx.SimulateRequest{Tx: protoTx}, false, ""},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
req, err := val.ClientCtx.JSONMarshaler.MarshalJSON(tc.req)
s.Require().NoError(err)
res, err := rest.PostRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/simulate", val.APIAddress), "application/json", req)
s.Require().NoError(err)
if tc.expErr {
s.Require().Contains(string(res), tc.expErrMsg)
} else {
var result tx.SimulateResponse
err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result)
s.Require().NoError(err)
// Check the result and gas used are correct.
s.Require().Equal(len(result.GetResult().GetEvents()), 6) // 1 coin recv, 1 coin spent,1 transfer, 3 messages.
s.Require().True(result.GetGasInfo().GetGasUsed() > 0) // Gas used sometimes change, just check it's not empty.
}
})
}
}
func (s IntegrationTestSuite) TestGetTxEvents_GRPC() {
testCases := []struct {
name string
req *tx.GetTxsEventRequest
expErr bool
expErrMsg string
}{
{
"nil request",
nil,
true, "request cannot be nil",
},
{
"empty request",
&tx.GetTxsEventRequest{},
true, "must declare at least one event to search",
},
{
"request with dummy event",
&tx.GetTxsEventRequest{Events: []string{"foobar"}},
true, "event foobar should be of the format: {eventType}.{eventAttribute}={value}",
},
{
"without pagination",
&tx.GetTxsEventRequest{
Events: []string{"message.action='/cosmos.bank.v1beta1.Msg/Send'"},
},
false, "",
},
{
"with pagination",
&tx.GetTxsEventRequest{
Events: []string{"message.action='/cosmos.bank.v1beta1.Msg/Send'"},
Pagination: &query.PageRequest{
CountTotal: false,
Offset: 0,
Limit: 1,
},
},
false, "",
},
{
"with multi events",
&tx.GetTxsEventRequest{
Events: []string{"message.action='/cosmos.bank.v1beta1.Msg/Send'", "message.module='bank'"},
},
false, "",
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
// Query the tx via gRPC.
grpcRes, err := s.queryClient.GetTxsEvent(context.Background(), tc.req)
if tc.expErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expErrMsg)
} else {
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)
}
})
}
}
func (s IntegrationTestSuite) TestGetTxEvents_GRPCGateway() {
val := s.network.Validators[0]
testCases := []struct {
name string
url string
expErr bool
expErrMsg string
}{
{
"empty params",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs", val.APIAddress),
true,
"must declare at least one event to search",
},
{
"without pagination",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s", val.APIAddress, "message.action='/cosmos.bank.v1beta1.Msg/Send'"),
false,
"",
},
{
"with pagination",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s&pagination.offset=%d&pagination.limit=%d", val.APIAddress, "message.action='/cosmos.bank.v1beta1.Msg/Send'", 0, 10),
false,
"",
},
{
"expect pass with multiple-events",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s&events=%s", val.APIAddress, "message.action='/cosmos.bank.v1beta1.Msg/Send'", "message.module='bank'"),
false,
"",
},
{
"expect pass with escape event",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=%s", val.APIAddress, "message.action%3D'%2Fcosmos.bank.v1beta1.Msg%2FSend'"),
false,
"",
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
res, err := rest.GetRequest(tc.url)
s.Require().NoError(err)
if tc.expErr {
s.Require().Contains(string(res), tc.expErrMsg)
} else {
var result tx.GetTxsEventResponse
err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result)
s.Require().NoError(err)
s.Require().GreaterOrEqual(len(result.Txs), 1)
s.Require().Equal("foobar", result.Txs[0].Body.Memo)
s.Require().NotZero(result.TxResponses[0].Height)
}
})
}
}
func (s IntegrationTestSuite) TestGetTx_GRPC() {
testCases := []struct {
name string
req *tx.GetTxRequest
expErr bool
expErrMsg string
}{
{"nil request", nil, true, "request cannot be nil"},
{"empty request", &tx.GetTxRequest{}, true, "transaction hash cannot be empty"},
{"request with dummy hash", &tx.GetTxRequest{Hash: "deadbeef"}, true, "tx (DEADBEEF) not found"},
{"good request", &tx.GetTxRequest{Hash: s.txRes.TxHash}, false, ""},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
// Query the tx via gRPC.
grpcRes, err := s.queryClient.GetTx(context.Background(), tc.req)
if tc.expErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expErrMsg)
} else {
s.Require().NoError(err)
s.Require().Equal("foobar", grpcRes.Tx.Body.Memo)
}
})
}
}
func (s IntegrationTestSuite) TestGetTx_GRPCGateway() {
val := s.network.Validators[0]
testCases := []struct {
name string
url string
expErr bool
expErrMsg string
}{
{
"empty params",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/", val.APIAddress),
true, "transaction hash cannot be empty",
},
{
"dummy hash",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", val.APIAddress, "deadbeef"),
true, "tx (DEADBEEF) not found",
},
{
"good hash",
fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", val.APIAddress, s.txRes.TxHash),
false, "",
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
res, err := rest.GetRequest(tc.url)
s.Require().NoError(err)
if tc.expErr {
s.Require().Contains(string(res), tc.expErrMsg)
} else {
var result tx.GetTxResponse
err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result)
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)
}
})
}
}
func (s IntegrationTestSuite) TestBroadcastTx_GRPC() {
val := s.network.Validators[0]
txBuilder := s.mkTxBuilder()
txBytes, err := val.ClientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
s.Require().NoError(err)
testCases := []struct {
name string
req *tx.BroadcastTxRequest
expErr bool
expErrMsg string
}{
{"nil request", nil, true, "request cannot be nil"},
{"empty request", &tx.BroadcastTxRequest{}, true, "invalid empty tx"},
{"no mode", &tx.BroadcastTxRequest{
TxBytes: txBytes,
}, true, "supported types: sync, async, block"},
{"valid request", &tx.BroadcastTxRequest{
Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC,
TxBytes: txBytes,
}, false, ""},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
// Broadcast the tx via gRPC via the validator's clientCtx (which goes
// through Tendermint).
grpcRes, err := s.queryClient.BroadcastTx(context.Background(), tc.req)
if tc.expErr {
s.Require().Error(err)
s.Require().Contains(err.Error(), tc.expErrMsg)
} else {
s.Require().NoError(err)
s.Require().Equal(uint32(0), grpcRes.TxResponse.Code)
}
})
}
}
func (s IntegrationTestSuite) TestBroadcastTx_GRPCGateway() {
val := s.network.Validators[0]
txBuilder := s.mkTxBuilder()
txBytes, err := val.ClientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
s.Require().NoError(err)
testCases := []struct {
name string
req *tx.BroadcastTxRequest
expErr bool
expErrMsg string
}{
{"empty request", &tx.BroadcastTxRequest{}, true, "invalid empty tx"},
{"no mode", &tx.BroadcastTxRequest{TxBytes: txBytes}, true, "supported types: sync, async, block"},
{"valid request", &tx.BroadcastTxRequest{
Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC,
TxBytes: txBytes,
}, false, ""},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
req, err := val.ClientCtx.JSONMarshaler.MarshalJSON(tc.req)
s.Require().NoError(err)
res, err := rest.PostRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/txs", val.APIAddress), "application/json", req)
s.Require().NoError(err)
if tc.expErr {
s.Require().Contains(string(res), tc.expErrMsg)
} else {
var result tx.BroadcastTxResponse
err = val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result)
s.Require().NoError(err)
s.Require().Equal(uint32(0), result.TxResponse.Code, "rawlog", result.TxResponse.RawLog)
}
})
}
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}
func (s IntegrationTestSuite) mkTxBuilder() client.TxBuilder {
val := s.network.Validators[0]
s.Require().NoError(s.network.WaitForNextBlock())
// prepare txBuilder with msg
txBuilder := val.ClientCtx.TxConfig.NewTxBuilder()
feeAmount := sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 10)}
gasLimit := testdata.NewTestGasLimit()
s.Require().NoError(
txBuilder.SetMsgs(&banktypes.MsgSend{
FromAddress: val.Address.String(),
ToAddress: val.Address.String(),
Amount: sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 10)},
}),
)
txBuilder.SetFeeAmount(feeAmount)
txBuilder.SetGasLimit(gasLimit)
// setup txFactory
txFactory := clienttx.Factory{}.
WithChainID(val.ClientCtx.ChainID).
WithKeybase(val.ClientCtx.Keyring).
WithTxConfig(val.ClientCtx.TxConfig).
WithSignMode(signing.SignMode_SIGN_MODE_DIRECT)
// Sign Tx.
err := authclient.SignTx(txFactory, val.ClientCtx, val.Moniker, txBuilder, false, true)
s.Require().NoError(err)
return txBuilder
}
// txBuilderToProtoTx converts a txBuilder into a proto tx.Tx.
func txBuilderToProtoTx(txBuilder client.TxBuilder) (*tx.Tx, error) { // nolint
protoProvider, ok := txBuilder.(authtx.ProtoTxProvider)
if !ok {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "expected proto tx builder, got %T", txBuilder)
}
return protoProvider.GetProtoTx(), nil
}