package rest_test import ( "fmt" "testing" "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/testutil" clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" "github.com/cosmos/cosmos-sdk/testutil/network" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/types/tx/signing" authclient "github.com/cosmos/cosmos-sdk/x/auth/client" authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil" ibccli "github.com/cosmos/cosmos-sdk/x/ibc/core/04-channel/client/cli" txtypes "github.com/cosmos/cosmos-sdk/types/tx" rest2 "github.com/cosmos/cosmos-sdk/x/auth/client/rest" "github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx" "github.com/cosmos/cosmos-sdk/x/bank/types" ) type IntegrationTestSuite struct { suite.Suite cfg network.Config network *network.Network stdTx legacytx.StdTx stdTxRes sdk.TxResponse } func (s *IntegrationTestSuite) SetupSuite() { s.T().Log("setting up integration test suite") cfg := network.DefaultConfig() cfg.NumValidators = 2 s.cfg = cfg s.network = network.New(s.T(), cfg) kb := s.network.Validators[0].ClientCtx.Keyring _, _, err := kb.NewMnemonic("newAccount", keyring.English, sdk.FullFundraiserPath, hd.Secp256k1) s.Require().NoError(err) _, err = s.network.WaitForHeight(1) s.Require().NoError(err) // Broadcast a StdTx used for tests. s.stdTx = s.createTestStdTx(s.network.Validators[0], 0, 1) res, err := s.broadcastReq(s.stdTx, "block") s.Require().NoError(err) // NOTE: this uses amino explicitly, don't migrate it! s.Require().NoError(s.cfg.LegacyAmino.UnmarshalJSON(res, &s.stdTxRes)) s.Require().Equal(uint32(0), s.stdTxRes.Code) s.Require().NoError(s.network.WaitForNextBlock()) } func (s *IntegrationTestSuite) TearDownSuite() { s.T().Log("tearing down integration test suite") s.network.Cleanup() } func mkTx() legacytx.StdTx { // NOTE: this uses StdTx explicitly, don't migrate it! return legacytx.StdTx{ Msgs: []sdk.Msg{&types.MsgSend{}}, Fee: legacytx.StdFee{ Amount: sdk.Coins{sdk.NewInt64Coin("foo", 10)}, Gas: 10000, }, Memo: "FOOBAR", } } func (s *IntegrationTestSuite) TestEncodeDecode() { var require = s.Require() val := s.network.Validators[0] stdTx := mkTx() // NOTE: this uses amino explicitly, don't migrate it! cdc := val.ClientCtx.LegacyAmino bz, err := cdc.MarshalJSON(stdTx) require.NoError(err) res, err := rest.PostRequest(fmt.Sprintf("%s/txs/encode", val.APIAddress), "application/json", bz) require.NoError(err) var encodeResp rest2.EncodeResp err = cdc.UnmarshalJSON(res, &encodeResp) require.NoError(err) bz, err = cdc.MarshalJSON(rest2.DecodeReq{Tx: encodeResp.Tx}) require.NoError(err) res, err = rest.PostRequest(fmt.Sprintf("%s/txs/decode", val.APIAddress), "application/json", bz) require.NoError(err) var respWithHeight rest.ResponseWithHeight err = cdc.UnmarshalJSON(res, &respWithHeight) require.NoError(err) var decodeResp rest2.DecodeResp err = cdc.UnmarshalJSON(respWithHeight.Result, &decodeResp) require.NoError(err) require.Equal(stdTx, legacytx.StdTx(decodeResp)) } func (s *IntegrationTestSuite) TestBroadcastTxRequest() { stdTx := mkTx() // we just test with async mode because this tx will fail - all we care about is that it got encoded and broadcast correctly res, err := s.broadcastReq(stdTx, "async") s.Require().NoError(err) var txRes sdk.TxResponse // NOTE: this uses amino explicitly, don't migrate it! s.Require().NoError(s.cfg.LegacyAmino.UnmarshalJSON(res, &txRes)) // we just check for a non-empty TxHash here, the actual hash will depend on the underlying tx configuration s.Require().NotEmpty(txRes.TxHash) } func (s *IntegrationTestSuite) TestQueryTxByHash() { val0 := s.network.Validators[0] // We broadcasted a StdTx in SetupSuite. // we just check for a non-empty TxHash here, the actual hash will depend on the underlying tx configuration s.Require().NotEmpty(s.stdTxRes.TxHash) // We now fetch the tx by hash on the `/tx/{hash}` route. txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val0.APIAddress, s.stdTxRes.TxHash)) s.Require().NoError(err) // txJSON should contain the whole tx, we just make sure that our custom // memo is there. s.Require().Contains(string(txJSON), s.stdTx.Memo) } func (s *IntegrationTestSuite) TestQueryTxByHeight() { val0 := s.network.Validators[0] // We broadcasted a StdTx in SetupSuite. // we just check for a non-empty height here, as we'll need to for querying. s.Require().NotEmpty(s.stdTxRes.Height) // We now fetch the tx on `/txs` route, filtering by `tx.height` txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs?limit=10&page=1&tx.height=%d", val0.APIAddress, s.stdTxRes.Height)) s.Require().NoError(err) // txJSON should contain the whole tx, we just make sure that our custom // memo is there. s.Require().Contains(string(txJSON), s.stdTx.Memo) } func (s *IntegrationTestSuite) TestQueryTxByHashWithServiceMessage() { val := s.network.Validators[0] sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10) // Right after this line, we're sending a tx. Might need to wait a block // to refresh sequences. s.Require().NoError(s.network.WaitForNextBlock()) out, err := bankcli.ServiceMsgSendExec( val.ClientCtx, val.Address, val.Address, sdk.NewCoins(sendTokens), 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), ) s.Require().NoError(err) var txRes sdk.TxResponse s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txRes)) s.Require().Equal(uint32(0), txRes.Code) s.Require().NoError(s.network.WaitForNextBlock()) txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val.APIAddress, txRes.TxHash)) s.Require().NoError(err) var txResAmino sdk.TxResponse s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &txResAmino)) stdTx, ok := txResAmino.Tx.GetCachedValue().(legacytx.StdTx) s.Require().True(ok) msgs := stdTx.GetMsgs() s.Require().Equal(len(msgs), 1) _, ok = msgs[0].(*types.MsgSend) s.Require().True(ok) } func (s *IntegrationTestSuite) TestMultipleSyncBroadcastTxRequests() { // First test transaction from validator should have sequence=1 (non-genesis tx) testCases := []struct { desc string sequence uint64 shouldErr bool }{ { "First tx (correct sequence)", 1, false, }, { "Second tx (correct sequence)", 2, false, }, { "Third tx (incorrect sequence)", 9, true, }, } for _, tc := range testCases { s.Run(fmt.Sprintf("Case %s", tc.desc), func() { // broadcast test with sync mode, as we want to run CheckTx to verify account sequence is correct stdTx := s.createTestStdTx(s.network.Validators[1], 1, tc.sequence) res, err := s.broadcastReq(stdTx, "sync") s.Require().NoError(err) var txRes sdk.TxResponse // NOTE: this uses amino explicitly, don't migrate it! s.Require().NoError(s.cfg.LegacyAmino.UnmarshalJSON(res, &txRes)) // we check for a exitCode=0, indicating a successful broadcast if tc.shouldErr { var sigVerifyFailureCode uint32 = 4 s.Require().Equal(sigVerifyFailureCode, txRes.Code, "Testcase '%s': Expected signature verification failure {Code: %d} from TxResponse. "+ "Found {Code: %d, RawLog: '%v'}", tc.desc, sigVerifyFailureCode, txRes.Code, txRes.RawLog, ) } else { s.Require().Equal(uint32(0), txRes.Code, "Testcase '%s': TxResponse errored unexpectedly. Err: {Code: %d, RawLog: '%v'}", tc.desc, txRes.Code, txRes.RawLog, ) } }) } } func (s *IntegrationTestSuite) createTestStdTx(val *network.Validator, accNum, sequence uint64) legacytx.StdTx { txConfig := legacytx.StdTxConfig{Cdc: s.cfg.LegacyAmino} msg := &types.MsgSend{ FromAddress: val.Address.String(), ToAddress: val.Address.String(), Amount: sdk.Coins{sdk.NewInt64Coin(fmt.Sprintf("%stoken", val.Moniker), 100)}, } // prepare txBuilder with msg txBuilder := txConfig.NewTxBuilder() feeAmount := sdk.Coins{sdk.NewInt64Coin(s.cfg.BondDenom, 10)} gasLimit := testdata.NewTestGasLimit() txBuilder.SetMsgs(msg) txBuilder.SetFeeAmount(feeAmount) txBuilder.SetGasLimit(gasLimit) txBuilder.SetMemo("foobar") // setup txFactory txFactory := tx.Factory{}. WithChainID(val.ClientCtx.ChainID). WithKeybase(val.ClientCtx.Keyring). WithTxConfig(txConfig). WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON). WithAccountNumber(accNum). WithSequence(sequence) // sign Tx (offline mode so we can manually set sequence number) err := authclient.SignTx(txFactory, val.ClientCtx, val.Moniker, txBuilder, true) s.Require().NoError(err) stdTx := txBuilder.GetTx().(legacytx.StdTx) return stdTx } func (s *IntegrationTestSuite) broadcastReq(stdTx legacytx.StdTx, mode string) ([]byte, error) { val := s.network.Validators[0] // NOTE: this uses amino explicitly, don't migrate it! cdc := val.ClientCtx.LegacyAmino req := rest2.BroadcastReq{ Tx: stdTx, Mode: mode, } bz, err := cdc.MarshalJSON(req) s.Require().NoError(err) return rest.PostRequest(fmt.Sprintf("%s/txs", val.APIAddress), "application/json", bz) } func (s *IntegrationTestSuite) TestLegacyRestErrMessages() { val := s.network.Validators[0] args := []string{ "121", // dummy port-id "21212121212", // dummy channel-id 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=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=foobar", flags.FlagMemo), } // created a dummy txn for IBC, eventually it fails. Our intension is to test the error message of querying a // message which is signed with proto, since IBC won't support legacy amino at all we are considering a message from IBC module. out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, ibccli.NewChannelCloseInitCmd(), args) s.Require().NoError(err) var txRes sdk.TxResponse s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &txRes)) s.Require().NoError(s.network.WaitForNextBlock()) // try to fetch the txn using legacy rest, this won't work since the ibc module doesn't support amino. txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val.APIAddress, txRes.TxHash)) s.Require().NoError(err) var errResp rest.ErrorResponse s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &errResp)) errMsg := "This transaction was created with the new SIGN_MODE_DIRECT signing method, " + "and therefore cannot be displayed via legacy REST handlers, please use CLI or directly query the Tendermint " + "RPC endpoint to query this transaction." s.Require().Contains(errResp.Error, errMsg) // try fetching the txn using gRPC req, it will fetch info since it has proto codec. grpcJSON, err := rest.GetRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/tx/%s", val.APIAddress, txRes.TxHash)) s.Require().NoError(err) var getTxRes txtypes.GetTxResponse s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(grpcJSON, &getTxRes)) s.Require().Equal(getTxRes.Tx.Body.Memo, "foobar") // generate broadcast only txn. args = append(args, fmt.Sprintf("--%s=true", flags.FlagGenerateOnly)) out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, ibccli.NewChannelCloseInitCmd(), args) s.Require().NoError(err) txFile, cleanup := testutil.WriteToNewTempFile(s.T(), string(out.Bytes())) txFileName := txFile.Name() s.T().Cleanup(cleanup) // encode the generated txn. out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, authcli.GetEncodeCommand(), []string{txFileName}) s.Require().NoError(err) bz, err := val.ClientCtx.LegacyAmino.MarshalJSON(rest2.DecodeReq{Tx: string(out.Bytes())}) s.Require().NoError(err) // try to decode the txn using legacy rest, it fails. res, err := rest.PostRequest(fmt.Sprintf("%s/txs/decode", val.APIAddress), "application/json", bz) s.Require().NoError(err) s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(res, &errResp)) s.Require().Contains(errResp.Error, errMsg) } func TestIntegrationTestSuite(t *testing.T) { suite.Run(t, new(IntegrationTestSuite)) }