Add tx broadcast gRPC endpoint (#7852)

* WIP tx/broadcast grpc endpoint

* fix lint

* fix proto lint

* Update service.proto

* resolve conflicts

* update service.proto

* Update service.proto

* review changes

* proto lint

* Switch to txraw

* Add check breaking at the end

* Fix broadcast

* Send Msg on SetupSuite

* Remove proto-check-breaking

* 1 validator in test

* Add grpc server tests for broadcast

* Fix grpc server tests

* Add some changes

* Add ress comments

* Add table tests for tx service

* Add test for mode

* Add simulate tests

* Add build flag back

* Revert custom stringer for enum

* Remove stray logs

* Use /txs/{hash}

Co-authored-by: Amaury Martiny <amaury.martiny@protonmail.com>
Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
This commit is contained in:
MD Aleem
2020-12-02 17:50:40 +00:00
committed by GitHub
co-authored by Amaury Martiny Aleksandr Bezobchuk
parent f57828c091
commit b6c8d5ea9f
12 changed files with 1146 additions and 206 deletions
+1 -1
View File
@@ -151,7 +151,7 @@ func QueryTxRequestHandlerFn(clientCtx client.Context) http.HandlerFunc {
rest.WriteErrorResponse(w, http.StatusNotFound, fmt.Sprintf("no transaction found with hash %s", hashHexStr))
}
err = checkSignModeError(clientCtx, output, "/cosmos/tx/v1beta1/tx/{txhash}")
err = checkSignModeError(clientCtx, output, "/cosmos/tx/v1beta1/txs/{txhash}")
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
+2 -2
View File
@@ -345,7 +345,7 @@ func (s *IntegrationTestSuite) broadcastReq(stdTx legacytx.StdTx, mode string) (
// testQueryIBCTx is a helper function to test querying txs which:
// - show an error message on legacy REST endpoints
// - succeed using gRPC
// In practise, we call this function on IBC txs.
// In practice, we call this function on IBC txs.
func (s *IntegrationTestSuite) testQueryIBCTx(txRes sdk.TxResponse, cmd *cobra.Command, args []string) {
val := s.network.Validators[0]
@@ -381,7 +381,7 @@ func (s *IntegrationTestSuite) testQueryIBCTx(txRes sdk.TxResponse, cmd *cobra.C
}
// 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))
grpcJSON, err := rest.GetRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", val.APIAddress, txRes.TxHash))
s.Require().NoError(err)
var getTxRes txtypes.GetTxResponse
+13 -1
View File
@@ -49,6 +49,10 @@ const (
// TxsByEvents implements the ServiceServer.TxsByEvents RPC method.
func (s txServer) GetTxsEvent(ctx context.Context, req *txtypes.GetTxsEventRequest) (*txtypes.GetTxsEventResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "request cannot be nil")
}
page, limit, err := pagination.ParsePagination(req.Pagination)
if err != nil {
return nil, err
@@ -113,7 +117,7 @@ func (s txServer) GetTxsEvent(ctx context.Context, req *txtypes.GetTxsEventReque
// Simulate implements the ServiceServer.Simulate RPC method.
func (s txServer) Simulate(ctx context.Context, req *txtypes.SimulateRequest) (*txtypes.SimulateResponse, error) {
if req.Tx == nil {
if req == nil || req.Tx == nil {
return nil, status.Error(codes.InvalidArgument, "invalid empty tx")
}
@@ -139,6 +143,10 @@ func (s txServer) Simulate(ctx context.Context, req *txtypes.SimulateRequest) (*
// GetTx implements the ServiceServer.GetTx RPC method.
func (s txServer) GetTx(ctx context.Context, req *txtypes.GetTxRequest) (*txtypes.GetTxResponse, error) {
if req == nil {
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 {
@@ -170,6 +178,10 @@ func (s txServer) GetTx(ctx context.Context, req *txtypes.GetTxRequest) (*txtype
}, nil
}
func (s txServer) BroadcastTx(ctx context.Context, req *txtypes.BroadcastTxRequest) (*txtypes.BroadcastTxResponse, error) {
return client.TxServiceBroadcast(ctx, s.clientCtx, req)
}
// RegisterTxService registers the tx service on the gRPC router.
func RegisterTxService(
qrt gogogrpc.Server,
+322 -127
View File
@@ -31,6 +31,7 @@ type IntegrationTestSuite struct {
network *network.Network
queryClient tx.ServiceClient
txRes sdk.TxResponse
}
func (s *IntegrationTestSuite) SetupSuite() {
@@ -41,67 +42,14 @@ func (s *IntegrationTestSuite) SetupSuite() {
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(s.network.Validators[0].ClientCtx)
}
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
func (s IntegrationTestSuite) TestSimulate() {
val := s.network.Validators[0]
// 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)
txBuilder.SetMemo("foobar")
// 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)
s.Require().NoError(err)
// Convert the txBuilder to a tx.Tx.
protoTx, err := txBuilderToProtoTx(txBuilder)
s.Require().NoError(err)
// Run the simulate gRPC query.
res, err := s.queryClient.Simulate(
context.Background(),
&tx.SimulateRequest{Tx: protoTx},
)
s.Require().NoError(err)
// Check the result and gas used are correct.
s.Require().Equal(len(res.GetResult().GetEvents()), 4) // 1 transfer, 3 messages.
s.Require().True(res.GetGasInfo().GetGasUsed() > 0) // Gas used sometimes change, just check it's not empty.
}
func (s IntegrationTestSuite) TestGetTxEvents() {
val := s.network.Validators[0]
s.queryClient = tx.NewServiceClient(val.ClientCtx)
// Create a new MsgSend tx from val to itself.
out, err := bankcli.MsgSendExec(
@@ -118,51 +66,161 @@ func (s IntegrationTestSuite) TestGetTxEvents() {
fmt.Sprintf("--%s=foobar", flags.FlagMemo),
)
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(val.ClientCtx.JSONMarshaler.UnmarshalJSON(out.Bytes(), &s.txRes))
s.Require().Equal(uint32(0), s.txRes.Code)
s.Require().NoError(s.network.WaitForNextBlock())
}
// Query the tx via gRPC empty params.
_, err = s.queryClient.GetTxsEvent(
context.Background(),
&tx.GetTxsEventRequest{},
)
s.Require().Error(err)
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
// Query the tx via gRPC.
grpcRes, err := s.queryClient.GetTxsEvent(
context.Background(),
&tx.GetTxsEventRequest{
Events: []string{"message.action=send"},
Pagination: &query.PageRequest{
CountTotal: false,
Offset: 0,
Limit: 1,
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()), 4) // 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()), 4) // 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=send"},
},
false, "",
},
)
s.Require().NoError(err)
s.Require().Equal(len(grpcRes.Txs), 1)
s.Require().Equal("foobar", grpcRes.Txs[0].Body.Memo)
// Query the tx via gRPC without pagination. This used to panic, see
// https://github.com/cosmos/cosmos-sdk/issues/8038.
grpcRes, err = s.queryClient.GetTxsEvent(
context.Background(),
&tx.GetTxsEventRequest{
Events: []string{"message.action=send"},
{
"with pagination",
&tx.GetTxsEventRequest{
Events: []string{"message.action=send"},
Pagination: &query.PageRequest{
CountTotal: false,
Offset: 0,
Limit: 1,
},
},
false, "",
},
)
// TODO Once https://github.com/cosmos/cosmos-sdk/pull/8029 is merged, this
// should not error anymore.
s.Require().NoError(err)
{
"with multi events",
&tx.GetTxsEventRequest{
Events: []string{"message.action=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)
}
})
}
}
rpcTests := []struct {
func (s IntegrationTestSuite) TestGetTxEvents_GRPCGateway() {
val := s.network.Validators[0]
testCases := []struct {
name string
url string
expectErr bool
expErr bool
expErrMsg string
}{
{
@@ -196,15 +254,16 @@ func (s IntegrationTestSuite) TestGetTxEvents() {
"",
},
}
for _, tc := range rpcTests {
for _, tc := range testCases {
s.Run(tc.name, func() {
res, err := rest.GetRequest(tc.url)
s.Require().NoError(err)
if tc.expectErr {
if tc.expErr {
s.Require().Contains(string(res), tc.expErrMsg)
} else {
var result tx.GetTxsEventResponse
val.ClientCtx.JSONMarshaler.UnmarshalJSON(res, &result)
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)
@@ -213,52 +272,188 @@ func (s IntegrationTestSuite) TestGetTxEvents() {
}
}
func (s IntegrationTestSuite) TestGetTx() {
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)
}
})
}
}
// 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),
)
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)
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())
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, ""},
}
// Query the tx via gRPC.
grpcRes, err := s.queryClient.GetTx(
context.Background(),
&tx.GetTxRequest{Hash: txRes.TxHash},
)
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)
s.Require().Equal("foobar", grpcRes.Tx.Body.Memo)
s.Require().NotZero(grpcRes.TxResponse.Height)
// Query the tx via grpc-gateway.
restRes, err := rest.GetRequest(fmt.Sprintf("%s/cosmos/tx/v1beta1/tx/%s", val.APIAddress, txRes.TxHash))
s.Require().NoError(err)
var getTxRes tx.GetTxResponse
s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(restRes, &getTxRes))
s.Require().Equal("foobar", grpcRes.Tx.Body.Memo)
s.Require().NotZero(grpcRes.TxResponse.Height)
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)
}
})
}
}
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)
s.Require().NoError(err)
return txBuilder
}
// txBuilderToProtoTx converts a txBuilder into a proto tx.Tx.
func txBuilderToProtoTx(txBuilder client.TxBuilder) (*tx.Tx, error) { // nolint
intoAnyTx, ok := txBuilder.(codectypes.IntoAny)