fix: remove legacy REST endpoints for broadcast & encode (v0.44.x) (#10041)
* fix: remove legacy REST endpoints for broadcast & encode * add changelog * update changelog * fix amino tx marshaling test * try to fix x/auth/client/rest tests * changing tx broadcast request type * remove auth client/rest_test.go Co-authored-by: Robert Zaremba <robert@zaremba.ch>
This commit is contained in:
parent
0155244d2a
commit
c1fc2a36d4
|
@ -56,7 +56,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||||
### API Breaking Changes
|
### API Breaking Changes
|
||||||
* (client/tx) [\#9421](https://github.com/cosmos/cosmos-sdk/pull/9421/) `BuildUnsignedTx`, `BuildSimTx`, `PrintUnsignedStdTx` functions are moved to
|
* (client/tx) [\#9421](https://github.com/cosmos/cosmos-sdk/pull/9421/) `BuildUnsignedTx`, `BuildSimTx`, `PrintUnsignedStdTx` functions are moved to
|
||||||
the Tx Factory as methods.
|
the Tx Factory as methods.
|
||||||
|
|
||||||
|
### Client Breaking Changes
|
||||||
|
* [\#10041](https://github.com/cosmos/cosmos-sdk/pull/10041) Remove broadcast & encode legacy REST endpoints. Please see the [REST Endpoints Migration guide](https://docs.cosmos.network/master/migrations/rest.html) to migrate to the new REST endpoints.
|
||||||
|
|
||||||
## [v0.43.0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.43.0) - 2021-08-10
|
## [v0.43.0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.43.0) - 2021-08-10
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/cosmos/cosmos-sdk/codec/types"
|
"github.com/cosmos/cosmos-sdk/codec/types"
|
||||||
"github.com/cosmos/cosmos-sdk/simapp"
|
"github.com/cosmos/cosmos-sdk/simapp"
|
||||||
"github.com/cosmos/cosmos-sdk/testutil/testdata"
|
"github.com/cosmos/cosmos-sdk/testutil/testdata"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/client/rest"
|
"github.com/cosmos/cosmos-sdk/x/auth/client/cli"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ func TestAminoCodecFullDecodeAndEncode(t *testing.T) {
|
||||||
require.Equal(t, string(marshaledTx), txSigned)
|
require.Equal(t, string(marshaledTx), txSigned)
|
||||||
|
|
||||||
// Marshalling/unmarshalling the tx wrapped in a struct should work.
|
// Marshalling/unmarshalling the tx wrapped in a struct should work.
|
||||||
txRequest := &rest.BroadcastReq{
|
txRequest := &cli.BroadcastReq{
|
||||||
Mode: "block",
|
Mode: "block",
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,16 @@ import (
|
||||||
signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing"
|
signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||||
"github.com/cosmos/cosmos-sdk/version"
|
"github.com/cosmos/cosmos-sdk/version"
|
||||||
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
|
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/client/rest"
|
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/signing"
|
"github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BroadcastReq defines a tx broadcasting request.
|
||||||
|
type BroadcastReq struct {
|
||||||
|
Tx legacytx.StdTx `json:"tx" yaml:"tx"`
|
||||||
|
Mode string `json:"mode" yaml:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetSignCommand returns the sign command
|
// GetSignCommand returns the sign command
|
||||||
func GetMultiSignCommand() *cobra.Command {
|
func GetMultiSignCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
@ -155,7 +161,7 @@ func makeMultiSignCmd() func(cmd *cobra.Command, args []string) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := rest.BroadcastReq{
|
req := BroadcastReq{
|
||||||
Tx: stdTx,
|
Tx: stdTx,
|
||||||
Mode: "block|sync|async",
|
Mode: "block|sync|async",
|
||||||
}
|
}
|
||||||
|
@ -338,7 +344,7 @@ func makeBatchMultisignCmd() func(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := rest.BroadcastReq{
|
req := BroadcastReq{
|
||||||
Tx: stdTx,
|
Tx: stdTx,
|
||||||
Mode: "block|sync|async",
|
Mode: "block|sync|async",
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||||
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
|
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/client/rest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -263,7 +262,7 @@ func makeSignCmd() func(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req := rest.BroadcastReq{
|
req := BroadcastReq{
|
||||||
Tx: stdTx,
|
Tx: stdTx,
|
||||||
Mode: "block|sync|async",
|
Mode: "block|sync|async",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
package rest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/client"
|
|
||||||
clientrest "github.com/cosmos/cosmos-sdk/client/rest"
|
|
||||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
|
||||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
|
||||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BroadcastReq defines a tx broadcasting request.
|
|
||||||
type BroadcastReq struct {
|
|
||||||
Tx legacytx.StdTx `json:"tx" yaml:"tx"`
|
|
||||||
Mode string `json:"mode" yaml:"mode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ codectypes.UnpackInterfacesMessage = BroadcastReq{}
|
|
||||||
|
|
||||||
// UnpackInterfaces implements the UnpackInterfacesMessage interface.
|
|
||||||
func (m BroadcastReq) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
|
|
||||||
return m.Tx.UnpackInterfaces(unpacker)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BroadcastTxRequest implements a tx broadcasting handler that is responsible
|
|
||||||
// for broadcasting a valid and signed tx to a full node. The tx can be
|
|
||||||
// broadcasted via a sync|async|block mechanism.
|
|
||||||
func BroadcastTxRequest(clientCtx client.Context) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req BroadcastReq
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if rest.CheckBadRequestError(w, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: amino is used intentionally here, don't migrate it!
|
|
||||||
err = clientCtx.LegacyAmino.UnmarshalJSON(body, &req)
|
|
||||||
if err != nil {
|
|
||||||
err := fmt.Errorf("this transaction cannot be broadcasted via legacy REST endpoints, because it does not support"+
|
|
||||||
" Amino serialization. Please either use CLI, gRPC, gRPC-gateway, or directly query the Tendermint RPC"+
|
|
||||||
" endpoint to broadcast this transaction. The new REST endpoint (via gRPC-gateway) is POST /cosmos/tx/v1beta1/txs."+
|
|
||||||
" Please also see the REST endpoints migration guide at %s for more info", clientrest.DeprecationURL)
|
|
||||||
if rest.CheckBadRequestError(w, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txBytes, err := tx.ConvertAndEncodeStdTx(clientCtx.TxConfig, req.Tx)
|
|
||||||
if rest.CheckInternalServerError(w, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clientCtx = clientCtx.WithBroadcastMode(req.Mode)
|
|
||||||
|
|
||||||
res, err := clientCtx.BroadcastTx(txBytes)
|
|
||||||
if rest.CheckInternalServerError(w, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rest.PostProcessResponseBare(w, clientCtx, res)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
package rest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/client"
|
|
||||||
clientrest "github.com/cosmos/cosmos-sdk/client/rest"
|
|
||||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
|
||||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EncodeResp defines a tx encoding response.
|
|
||||||
type EncodeResp struct {
|
|
||||||
Tx string `json:"tx" yaml:"tx"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrEncodeDecode is the error to show when encoding/decoding txs that are not
|
|
||||||
// amino-serializable (e.g. IBC txs).
|
|
||||||
var ErrEncodeDecode error = fmt.Errorf("this endpoint does not support txs that are not serializable"+
|
|
||||||
" via Amino, such as txs that contain IBC `Msg`s. For more info, please refer to our"+
|
|
||||||
" REST migration guide at %s", clientrest.DeprecationURL)
|
|
||||||
|
|
||||||
// EncodeTxRequestHandlerFn returns the encode tx REST handler. In particular,
|
|
||||||
// it takes a json-formatted transaction, encodes it to the Amino wire protocol,
|
|
||||||
// and responds with base64-encoded bytes.
|
|
||||||
func EncodeTxRequestHandlerFn(clientCtx client.Context) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req legacytx.StdTx
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if rest.CheckBadRequestError(w, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: amino is used intentionally here, don't migrate it
|
|
||||||
err = clientCtx.LegacyAmino.UnmarshalJSON(body, &req)
|
|
||||||
// If there's an unmarshalling error, we assume that it's because we're
|
|
||||||
// using amino to unmarshal a non-amino tx.
|
|
||||||
if err != nil {
|
|
||||||
if rest.CheckBadRequestError(w, ErrEncodeDecode) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-encode it in the chain's native binary format
|
|
||||||
txBytes, err := tx.ConvertAndEncodeStdTx(clientCtx.TxConfig, req)
|
|
||||||
if rest.CheckInternalServerError(w, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64 encode the encoded tx bytes
|
|
||||||
txBytesBase64 := base64.StdEncoding.EncodeToString(txBytes)
|
|
||||||
|
|
||||||
response := EncodeResp{Tx: txBytesBase64}
|
|
||||||
|
|
||||||
// NOTE: amino is set intentionally here, don't migrate it
|
|
||||||
rest.PostProcessResponseBare(w, clientCtx, response)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,5 @@ func RegisterTxRoutes(clientCtx client.Context, rtr *mux.Router) {
|
||||||
r := rest.WithHTTPDeprecationHeaders(rtr)
|
r := rest.WithHTTPDeprecationHeaders(rtr)
|
||||||
r.HandleFunc("/txs/{hash}", QueryTxRequestHandlerFn(clientCtx)).Methods("GET")
|
r.HandleFunc("/txs/{hash}", QueryTxRequestHandlerFn(clientCtx)).Methods("GET")
|
||||||
r.HandleFunc("/txs", QueryTxsRequestHandlerFn(clientCtx)).Methods("GET")
|
r.HandleFunc("/txs", QueryTxsRequestHandlerFn(clientCtx)).Methods("GET")
|
||||||
r.HandleFunc("/txs", BroadcastTxRequest(clientCtx)).Methods("POST")
|
|
||||||
r.HandleFunc("/txs/encode", EncodeTxRequestHandlerFn(clientCtx)).Methods("POST")
|
|
||||||
r.HandleFunc("/txs/decode", DecodeTxRequestHandlerFn(clientCtx)).Methods("POST")
|
r.HandleFunc("/txs/decode", DecodeTxRequestHandlerFn(clientCtx)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,534 +0,0 @@
|
||||||
// +build norace
|
|
||||||
|
|
||||||
package rest_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"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"
|
|
||||||
kmultisig "github.com/cosmos/cosmos-sdk/crypto/keys/multisig"
|
|
||||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
|
||||||
"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"
|
|
||||||
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
|
|
||||||
"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"
|
|
||||||
authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest"
|
|
||||||
authtest "github.com/cosmos/cosmos-sdk/x/auth/client/testutil"
|
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
|
||||||
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
|
|
||||||
"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, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
account1, _, err := kb.NewMnemonic("newAccount1", keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
account2, _, err := kb.NewMnemonic("newAccount2", keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
multi := kmultisig.NewLegacyAminoPubKey(2, []cryptotypes.PubKey{account1.GetPubKey(), account2.GetPubKey()})
|
|
||||||
_, err = kb.SaveMultisig("multi", multi)
|
|
||||||
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 mkStdTx() 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 := mkStdTx()
|
|
||||||
|
|
||||||
// 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 authrest.EncodeResp
|
|
||||||
err = cdc.UnmarshalJSON(res, &encodeResp)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
bz, err = cdc.MarshalJSON(authrest.DecodeReq(encodeResp))
|
|
||||||
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 authrest.DecodeResp
|
|
||||||
err = cdc.UnmarshalJSON(respWithHeight.Result, &decodeResp)
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal(stdTx, legacytx.StdTx(decodeResp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationTestSuite) TestQueryAccountWithColon() {
|
|
||||||
val := s.network.Validators[0]
|
|
||||||
// This address is not a valid simapp address! It is only used to test that addresses with
|
|
||||||
// colon don't 501. See
|
|
||||||
// https://github.com/cosmos/cosmos-sdk/issues/8650
|
|
||||||
addrWithColon := "cosmos:1m4f6lwd9eh8e5nxt0h00d46d3fr03apfh8qf4g"
|
|
||||||
|
|
||||||
res, err := rest.GetRequest(fmt.Sprintf("%s/cosmos/auth/v1beta1/accounts/%s", val.APIAddress, addrWithColon))
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.Require().Contains(string(res), "decoding bech32 failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationTestSuite) TestBroadcastTxRequest() {
|
|
||||||
stdTx := mkStdTx()
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to test querying txs. We will use it to query StdTx and service `Msg`s.
|
|
||||||
func (s *IntegrationTestSuite) testQueryTx(txHeight int64, txHash, txRecipient string) {
|
|
||||||
val0 := s.network.Validators[0]
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
malleate func() *sdk.TxResponse
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"Query by hash",
|
|
||||||
func() *sdk.TxResponse {
|
|
||||||
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs/%s", val0.APIAddress, txHash))
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
var txResAmino sdk.TxResponse
|
|
||||||
s.Require().NoError(val0.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &txResAmino))
|
|
||||||
return &txResAmino
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Query by height",
|
|
||||||
func() *sdk.TxResponse {
|
|
||||||
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs?limit=10&page=1&tx.height=%d", val0.APIAddress, txHeight))
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
var searchtxResult sdk.SearchTxsResult
|
|
||||||
s.Require().NoError(val0.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &searchtxResult))
|
|
||||||
s.Require().Len(searchtxResult.Txs, 1)
|
|
||||||
return searchtxResult.Txs[0]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Query by event (transfer.recipient)",
|
|
||||||
func() *sdk.TxResponse {
|
|
||||||
txJSON, err := rest.GetRequest(fmt.Sprintf("%s/txs?transfer.recipient=%s", val0.APIAddress, txRecipient))
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
var searchtxResult sdk.SearchTxsResult
|
|
||||||
s.Require().NoError(val0.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &searchtxResult))
|
|
||||||
s.Require().Len(searchtxResult.Txs, 1)
|
|
||||||
return searchtxResult.Txs[0]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
s.Run(fmt.Sprintf("Case %s", tc.desc), func() {
|
|
||||||
txResponse := tc.malleate()
|
|
||||||
|
|
||||||
// Check that the height is correct.
|
|
||||||
s.Require().Equal(txHeight, txResponse.Height)
|
|
||||||
|
|
||||||
// Check that the events are correct.
|
|
||||||
s.Require().Contains(
|
|
||||||
txResponse.RawLog,
|
|
||||||
fmt.Sprintf("{\"key\":\"recipient\",\"value\":\"%s\"}", txRecipient),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check that the Msg is correct.
|
|
||||||
stdTx, ok := txResponse.Tx.GetCachedValue().(legacytx.StdTx)
|
|
||||||
s.Require().True(ok)
|
|
||||||
msgs := stdTx.GetMsgs()
|
|
||||||
s.Require().Equal(len(msgs), 1)
|
|
||||||
msg, ok := msgs[0].(*types.MsgSend)
|
|
||||||
s.Require().True(ok)
|
|
||||||
s.Require().Equal(txRecipient, msg.ToAddress)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationTestSuite) TestQueryLegacyStdTx() {
|
|
||||||
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)
|
|
||||||
|
|
||||||
s.testQueryTx(s.stdTxRes.Height, s.stdTxRes.TxHash, val0.Address.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *IntegrationTestSuite) TestQueryTx() {
|
|
||||||
val := s.network.Validators[0]
|
|
||||||
|
|
||||||
sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 10)
|
|
||||||
_, _, addr := testdata.KeyTestPubAddr()
|
|
||||||
|
|
||||||
// Might need to wait a block to refresh sequences from previous setups.
|
|
||||||
s.Require().NoError(s.network.WaitForNextBlock())
|
|
||||||
|
|
||||||
out, err := bankcli.MsgSendExec(
|
|
||||||
val.ClientCtx,
|
|
||||||
val.Address,
|
|
||||||
addr,
|
|
||||||
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.Codec.UnmarshalJSON(out.Bytes(), &txRes))
|
|
||||||
s.Require().Equal(uint32(0), txRes.Code)
|
|
||||||
|
|
||||||
s.Require().NoError(s.network.WaitForNextBlock())
|
|
||||||
|
|
||||||
s.testQueryTx(txRes.Height, txRes.TxHash, addr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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 := authrest.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testQueryIBCTx is a helper function to test querying txs which:
|
|
||||||
// - show an error message on legacy REST endpoints
|
|
||||||
// - succeed using gRPC
|
|
||||||
// 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]
|
|
||||||
|
|
||||||
errMsg := "this transaction cannot be displayed via legacy REST endpoints, because it does not support" +
|
|
||||||
" Amino serialization. Please either use CLI, gRPC, gRPC-gateway, or directly query the Tendermint RPC" +
|
|
||||||
" endpoint to query this transaction. The new REST endpoint (via gRPC-gateway) is "
|
|
||||||
|
|
||||||
// Test that legacy endpoint return the above error message on IBC txs.
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
url string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"Query by hash",
|
|
||||||
fmt.Sprintf("%s/txs/%s", val.APIAddress, txRes.TxHash),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Query by height",
|
|
||||||
fmt.Sprintf("%s/txs?tx.height=%d", val.APIAddress, txRes.Height),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
s.Run(fmt.Sprintf("Case %s", tc.desc), func() {
|
|
||||||
txJSON, err := rest.GetRequest(tc.url)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
var errResp rest.ErrorResponse
|
|
||||||
s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(txJSON, &errResp))
|
|
||||||
|
|
||||||
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/txs/%s", val.APIAddress, txRes.TxHash))
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
var getTxRes txtypes.GetTxResponse
|
|
||||||
s.Require().NoError(val.ClientCtx.Codec.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, cmd, args)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
txFile := testutil.WriteToNewTempFile(s.T(), string(out.Bytes()))
|
|
||||||
txFileName := txFile.Name()
|
|
||||||
|
|
||||||
// encode the generated txn.
|
|
||||||
out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, authcli.GetEncodeCommand(), []string{txFileName})
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
bz, err := val.ClientCtx.LegacyAmino.MarshalJSON(authrest.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)
|
|
||||||
|
|
||||||
var errResp rest.ErrorResponse
|
|
||||||
s.Require().NoError(val.ClientCtx.LegacyAmino.UnmarshalJSON(res, &errResp))
|
|
||||||
s.Require().Contains(errResp.Error, errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLegacyMultiSig creates a legacy multisig transaction, and makes sure
|
|
||||||
// we can query it via the legacy REST endpoint.
|
|
||||||
// ref: https://github.com/cosmos/cosmos-sdk/issues/8679
|
|
||||||
func (s *IntegrationTestSuite) TestLegacyMultisig() {
|
|
||||||
val1 := *s.network.Validators[0]
|
|
||||||
|
|
||||||
// Generate 2 accounts and a multisig.
|
|
||||||
account1, err := val1.ClientCtx.Keyring.Key("newAccount1")
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
account2, err := val1.ClientCtx.Keyring.Key("newAccount2")
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
multisigInfo, err := val1.ClientCtx.Keyring.Key("multi")
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
// Send coins from validator to multisig.
|
|
||||||
sendTokens := sdk.NewInt64Coin(s.cfg.BondDenom, 1000)
|
|
||||||
_, err = bankcli.MsgSendExec(
|
|
||||||
val1.ClientCtx,
|
|
||||||
val1.Address,
|
|
||||||
multisigInfo.GetAddress(),
|
|
||||||
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(s.network.WaitForNextBlock())
|
|
||||||
|
|
||||||
// Generate multisig transaction to a random address.
|
|
||||||
_, _, recipient := testdata.KeyTestPubAddr()
|
|
||||||
multiGeneratedTx, err := bankcli.MsgSendExec(
|
|
||||||
val1.ClientCtx,
|
|
||||||
multisigInfo.GetAddress(),
|
|
||||||
recipient,
|
|
||||||
sdk.NewCoins(
|
|
||||||
sdk.NewInt64Coin(s.cfg.BondDenom, 5),
|
|
||||||
),
|
|
||||||
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("--%s=true", flags.FlagGenerateOnly),
|
|
||||||
)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
// Save tx to file
|
|
||||||
multiGeneratedTxFile := testutil.WriteToNewTempFile(s.T(), multiGeneratedTx.String())
|
|
||||||
|
|
||||||
// Sign with account1
|
|
||||||
val1.ClientCtx.HomeDir = strings.Replace(val1.ClientCtx.HomeDir, "simd", "simcli", 1)
|
|
||||||
account1Signature, err := authtest.TxSignExec(val1.ClientCtx, account1.GetAddress(), multiGeneratedTxFile.Name(), "--multisig", multisigInfo.GetAddress().String())
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
sign1File := testutil.WriteToNewTempFile(s.T(), account1Signature.String())
|
|
||||||
|
|
||||||
// Sign with account1
|
|
||||||
account2Signature, err := authtest.TxSignExec(val1.ClientCtx, account2.GetAddress(), multiGeneratedTxFile.Name(), "--multisig", multisigInfo.GetAddress().String())
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
sign2File := testutil.WriteToNewTempFile(s.T(), account2Signature.String())
|
|
||||||
|
|
||||||
// Does not work in offline mode.
|
|
||||||
_, err = authtest.TxMultiSignExec(val1.ClientCtx, multisigInfo.GetName(), multiGeneratedTxFile.Name(), "--offline", sign1File.Name(), sign2File.Name())
|
|
||||||
s.Require().EqualError(err, fmt.Sprintf("couldn't verify signature for address %s", account1.GetAddress()))
|
|
||||||
|
|
||||||
val1.ClientCtx.Offline = false
|
|
||||||
multiSigWith2Signatures, err := authtest.TxMultiSignExec(val1.ClientCtx, multisigInfo.GetName(), multiGeneratedTxFile.Name(), sign1File.Name(), sign2File.Name())
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
// Write the output to disk
|
|
||||||
signedTxFile := testutil.WriteToNewTempFile(s.T(), multiSigWith2Signatures.String())
|
|
||||||
|
|
||||||
_, err = authtest.TxValidateSignaturesExec(val1.ClientCtx, signedTxFile.Name())
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
val1.ClientCtx.BroadcastMode = flags.BroadcastBlock
|
|
||||||
out, err := authtest.TxBroadcastExec(val1.ClientCtx, signedTxFile.Name())
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
s.Require().NoError(s.network.WaitForNextBlock())
|
|
||||||
|
|
||||||
var txRes sdk.TxResponse
|
|
||||||
err = val1.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &txRes)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.Require().Equal(uint32(0), txRes.Code)
|
|
||||||
|
|
||||||
s.testQueryTx(txRes.Height, txRes.TxHash, recipient.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationTestSuite(t *testing.T) {
|
|
||||||
suite.Run(t, new(IntegrationTestSuite))
|
|
||||||
}
|
|
|
@ -29,7 +29,6 @@ import (
|
||||||
"github.com/cosmos/cosmos-sdk/types/tx"
|
"github.com/cosmos/cosmos-sdk/types/tx"
|
||||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||||
authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
|
authcli "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
|
||||||
authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest"
|
|
||||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||||
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
|
bankcli "github.com/cosmos/cosmos-sdk/x/bank/client/testutil"
|
||||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||||
|
@ -226,7 +225,7 @@ func (s *IntegrationTestSuite) TestCLISignAminoJSON() {
|
||||||
"--amino=true", signModeAminoFlag)
|
"--amino=true", signModeAminoFlag)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
var txAmino authrest.BroadcastReq
|
var txAmino authcli.BroadcastReq
|
||||||
err = val1.ClientCtx.LegacyAmino.UnmarshalJSON(res.Bytes(), &txAmino)
|
err = val1.ClientCtx.LegacyAmino.UnmarshalJSON(res.Bytes(), &txAmino)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.Len(txAmino.Tx.Signatures, 2)
|
require.Len(txAmino.Tx.Signatures, 2)
|
||||||
|
|
Loading…
Reference in New Issue