Some checks failed
CodeQL / Analyze (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Has been cancelled
352 lines
14 KiB
Go
352 lines
14 KiB
Go
package testsuite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cosmos/interchaintest/v10/chain/cosmos"
|
|
"github.com/cosmos/interchaintest/v10/ibc"
|
|
test "github.com/cosmos/interchaintest/v10/testutil"
|
|
|
|
errorsmod "cosmossdk.io/errors"
|
|
sdkmath "cosmossdk.io/math"
|
|
|
|
"git.cw.tr/mukan-network/mukan-sdk/client"
|
|
"git.cw.tr/mukan-network/mukan-sdk/client/tx"
|
|
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
|
|
txtypes "git.cw.tr/mukan-network/mukan-sdk/types/tx"
|
|
signingtypes "git.cw.tr/mukan-network/mukan-sdk/types/tx/signing"
|
|
authtx "git.cw.tr/mukan-network/mukan-sdk/x/auth/tx"
|
|
govtypesv1 "git.cw.tr/mukan-network/mukan-sdk/x/gov/types/v1"
|
|
govtypesv1beta1 "git.cw.tr/mukan-network/mukan-sdk/x/gov/types/v1beta1"
|
|
|
|
abci "git.cw.tr/mukan-network/mukan-consensus/abci/types"
|
|
|
|
"github.com/cosmos/ibc-go/e2e/testsuite/query"
|
|
"github.com/cosmos/ibc-go/e2e/testsuite/sanitize"
|
|
"github.com/cosmos/ibc-go/e2e/testvalues"
|
|
clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
|
|
)
|
|
|
|
// BroadcastMessages broadcasts the provided messages to the given chain and signs them on behalf of the provided user.
|
|
// Once the broadcast response is returned, we wait for a few blocks to be created on the chain the message was broadcast to.
|
|
func (s *E2ETestSuite) BroadcastMessages(ctx context.Context, chain ibc.Chain, user ibc.Wallet, msgs ...sdk.Msg) sdk.TxResponse {
|
|
cosmosChain, ok := chain.(*cosmos.CosmosChain)
|
|
if !ok {
|
|
panic("BroadcastMessages expects a cosmos.CosmosChain")
|
|
}
|
|
|
|
broadcaster := cosmos.NewBroadcaster(s.T(), cosmosChain)
|
|
|
|
// strip out any fields that may not be supported for the given chain version.
|
|
msgs = sanitize.Messages(cosmosChain.Nodes()[0].Image.Version, msgs...)
|
|
|
|
broadcaster.ConfigureClientContextOptions(func(clientContext client.Context) client.Context {
|
|
// use a codec with all the types our tests care about registered.
|
|
// BroadcastTx will deserialize the response and will not be able to otherwise.
|
|
cdc := Codec()
|
|
return clientContext.WithCodec(cdc).WithTxConfig(authtx.NewTxConfig(cdc, []signingtypes.SignMode{signingtypes.SignMode_SIGN_MODE_DIRECT}))
|
|
})
|
|
|
|
broadcaster.ConfigureFactoryOptions(func(factory tx.Factory) tx.Factory {
|
|
return factory.WithGas(DefaultGasValue)
|
|
})
|
|
|
|
// Retry the operation a few times if the user signing the transaction is a relayer. (See issue #3264)
|
|
var resp sdk.TxResponse
|
|
var err error
|
|
broadcastFunc := func() (sdk.TxResponse, error) {
|
|
return cosmos.BroadcastTx(ctx, broadcaster, user, msgs...)
|
|
}
|
|
if s.relayerWallets.ContainsRelayer(s.T().Name(), user) {
|
|
// Retry five times, the value of 5 chosen is arbitrary.
|
|
resp, err = s.retryNtimes(broadcastFunc, 5)
|
|
} else {
|
|
resp, err = broadcastFunc()
|
|
}
|
|
s.Require().NoError(err)
|
|
|
|
s.Require().NoError(test.WaitForBlocks(ctx, 2, chain))
|
|
s.T().Logf("blocks created on chain %s", chain.Config().ChainID)
|
|
return resp
|
|
}
|
|
|
|
// retryNtimes retries the provided function up to the provided number of attempts.
|
|
func (s *E2ETestSuite) retryNtimes(f func() (sdk.TxResponse, error), attempts int) (sdk.TxResponse, error) {
|
|
// Ignore account sequence mismatch errors.
|
|
retryMessages := []string{"account sequence mismatch"}
|
|
var resp sdk.TxResponse
|
|
var err error
|
|
// If the response's raw log doesn't contain any of the allowed prefixes we return, else, we retry.
|
|
for range attempts {
|
|
resp, err = f()
|
|
if err != nil {
|
|
return sdk.TxResponse{}, err
|
|
}
|
|
// If the response's raw log doesn't contain any of the allowed prefixes we return, else, we retry.
|
|
if !slices.ContainsFunc(retryMessages, func(s string) bool { return strings.Contains(resp.RawLog, s) }) {
|
|
return resp, err
|
|
}
|
|
s.T().Logf("retrying tx due to non deterministic failure: %+v", resp)
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
// AssertTxFailure verifies that an sdk.TxResponse has failed.
|
|
func (s *E2ETestSuite) AssertTxFailure(resp sdk.TxResponse, expectedError *errorsmod.Error, alternativeError ...*errorsmod.Error) {
|
|
errorMsg := fmt.Sprintf("%+v", resp)
|
|
// In older versions, the codespace and abci codes were different. So in compatibility tests
|
|
// we can not make assertions on them.
|
|
if GetChainATag() == GetChainBTag() {
|
|
s.Require().Equal(expectedError.ABCICode(), resp.Code, errorMsg)
|
|
s.Require().Equal(expectedError.Codespace(), resp.Codespace, errorMsg)
|
|
}
|
|
// Verify that the error message contains the expected error message or one of the alternative error messages.
|
|
if strings.Contains(resp.RawLog, expectedError.Error()) {
|
|
return
|
|
}
|
|
|
|
for _, altErr := range alternativeError {
|
|
if strings.Contains(resp.RawLog, altErr.Error()) {
|
|
return
|
|
}
|
|
}
|
|
s.Require().FailNow(fmt.Sprintf("expected error: %s, got: %s", expectedError.Error(), resp.RawLog))
|
|
}
|
|
|
|
// AssertTxSuccess verifies that an sdk.TxResponse has succeeded.
|
|
func (s *E2ETestSuite) AssertTxSuccess(resp sdk.TxResponse) {
|
|
errorMsg := addDebuggingInformation(fmt.Sprintf("%+v", resp))
|
|
s.Require().Equal(resp.Code, uint32(0), errorMsg)
|
|
s.Require().NotEmpty(resp.TxHash, errorMsg)
|
|
s.Require().NotEqual(int64(0), resp.GasUsed, errorMsg)
|
|
s.Require().NotEqual(int64(0), resp.GasWanted, errorMsg)
|
|
s.Require().NotEmpty(resp.Events, errorMsg)
|
|
s.Require().NotEmpty(resp.Data, errorMsg)
|
|
}
|
|
|
|
// addDebuggingInformation adds additional debugging information to the error message
|
|
// based on common types of errors that can occur.
|
|
func addDebuggingInformation(errorMsg string) string {
|
|
if strings.Contains(errorMsg, "errUnknownField") {
|
|
errorMsg += `
|
|
|
|
This error is likely due to a new an unrecognized proto field being provided to a chain using an older version of the sdk.
|
|
If this is a compatibility test, ensure that the fields are being sanitized in the sanitize.Messages function.
|
|
|
|
`
|
|
}
|
|
return errorMsg
|
|
}
|
|
|
|
// ExecuteAndPassGovV1Proposal submits a v1 governance proposal using the provided user and message and uses all validators
|
|
// to vote yes on the proposal. It ensures the proposal successfully passes.
|
|
func (s *E2ETestSuite) ExecuteAndPassGovV1Proposal(ctx context.Context, msg sdk.Msg, chain ibc.Chain, user ibc.Wallet) {
|
|
err := s.ExecuteGovV1Proposal(ctx, msg, chain, user)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// ExecuteGovV1Proposal submits a v1 governance proposal using the provided user and message and uses all validators
|
|
// to vote yes on the proposal.
|
|
func (s *E2ETestSuite) ExecuteGovV1Proposal(ctx context.Context, msg sdk.Msg, chain ibc.Chain, user ibc.Wallet) error {
|
|
cosmosChain, ok := chain.(*cosmos.CosmosChain)
|
|
if !ok {
|
|
panic("ExecuteAndPassGovV1Proposal must be passed a cosmos.CosmosChain")
|
|
}
|
|
|
|
sender, err := sdk.AccAddressFromBech32(user.FormattedAddress())
|
|
s.Require().NoError(err)
|
|
|
|
proposalID := s.proposalIDs[cosmosChain.Config().ChainID]
|
|
defer func() {
|
|
s.proposalIDs[cosmosChain.Config().ChainID] = proposalID + 1
|
|
}()
|
|
|
|
msgs := []sdk.Msg{msg}
|
|
|
|
msgSubmitProposal, err := govtypesv1.NewMsgSubmitProposal(
|
|
msgs,
|
|
sdk.NewCoins(sdk.NewCoin(cosmosChain.Config().Denom, sdkmath.NewInt(testvalues.DefaultGovV1ProposalTokenAmount))),
|
|
sender.String(),
|
|
"",
|
|
fmt.Sprintf("e2e gov proposal: %d", proposalID),
|
|
fmt.Sprintf("executing gov proposal %d", proposalID),
|
|
false,
|
|
)
|
|
s.Require().NoError(err)
|
|
|
|
s.T().Logf("submitting proposal with ID: %d", proposalID)
|
|
resp := s.BroadcastMessages(ctx, cosmosChain, user, msgSubmitProposal)
|
|
s.AssertTxSuccess(resp)
|
|
|
|
s.Require().NoError(cosmosChain.VoteOnProposalAllValidators(ctx, proposalID, cosmos.ProposalVoteYes))
|
|
|
|
s.T().Logf("validators voted %s on proposal with ID: %d", cosmos.ProposalVoteYes, proposalID)
|
|
return s.waitForGovV1ProposalToPass(ctx, cosmosChain, proposalID)
|
|
}
|
|
|
|
// waitForGovV1ProposalToPass polls for the entire voting period to see if the proposal has passed.
|
|
// if the proposal has not passed within the duration of the voting period, an error is returned.
|
|
func (s *E2ETestSuite) waitForGovV1ProposalToPass(ctx context.Context, chain ibc.Chain, proposalID uint64) error {
|
|
var govProposal *govtypesv1.Proposal
|
|
// poll for the query for the entire voting period to see if the proposal has passed.
|
|
err := test.WaitForCondition(testvalues.VotingPeriod, 10*time.Second, func() (bool, error) {
|
|
s.T().Logf("waiting for proposal with ID: %d to pass", proposalID)
|
|
proposalResp, err := query.GRPCQuery[govtypesv1.QueryProposalResponse](ctx, chain, &govtypesv1.QueryProposalRequest{
|
|
ProposalId: proposalID,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
govProposal = proposalResp.Proposal
|
|
return govProposal.Status == govtypesv1.StatusPassed, nil
|
|
})
|
|
|
|
// in the case of a failed proposal, we wrap the polling error with additional information about why the proposal failed.
|
|
if err != nil && govProposal.FailedReason != "" {
|
|
err = errorsmod.Wrap(err, govProposal.FailedReason)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ExecuteAndPassGovV1Beta1Proposal submits the given v1beta1 governance proposal using the provided user and uses all validators to vote yes on the proposal.
|
|
// It ensures the proposal successfully passes.
|
|
func (s *E2ETestSuite) ExecuteAndPassGovV1Beta1Proposal(ctx context.Context, chain ibc.Chain, user ibc.Wallet, content govtypesv1beta1.Content) {
|
|
cosmosChain, ok := chain.(*cosmos.CosmosChain)
|
|
if !ok {
|
|
panic("ExecuteAndPassGovV1Beta1Proposal must be passed a cosmos.CosmosChain")
|
|
}
|
|
|
|
txResp := s.ExecuteGovV1Beta1Proposal(ctx, cosmosChain, user, content)
|
|
s.AssertTxSuccess(txResp)
|
|
|
|
var submitProposalResponse govtypesv1beta1.MsgSubmitProposalResponse
|
|
s.Require().NoError(UnmarshalMsgResponses(txResp, &submitProposalResponse))
|
|
|
|
proposalID := submitProposalResponse.ProposalId
|
|
defer func() {
|
|
s.proposalIDs[chain.Config().ChainID] = proposalID + 1
|
|
}()
|
|
|
|
proposalResp, err := query.GRPCQuery[govtypesv1beta1.QueryProposalResponse](ctx, cosmosChain, &govtypesv1beta1.QueryProposalRequest{
|
|
ProposalId: proposalID,
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
proposal := proposalResp.Proposal
|
|
s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposal.Status)
|
|
|
|
err = cosmosChain.VoteOnProposalAllValidators(ctx, proposalID, cosmos.ProposalVoteYes)
|
|
s.Require().NoError(err)
|
|
|
|
// ensure voting period has not passed before validators finished voting
|
|
proposalResp, err = query.GRPCQuery[govtypesv1beta1.QueryProposalResponse](ctx, cosmosChain, &govtypesv1beta1.QueryProposalRequest{
|
|
ProposalId: proposalID,
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
proposal = proposalResp.Proposal
|
|
s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposal.Status)
|
|
|
|
err = s.waitForGovV1Beta1ProposalToPass(ctx, cosmosChain, proposalID)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// waitForGovV1Beta1ProposalToPass polls for the entire voting period to see if the proposal has passed.
|
|
// if the proposal has not passed within the duration of the voting period, an error is returned.
|
|
func (*E2ETestSuite) waitForGovV1Beta1ProposalToPass(ctx context.Context, chain ibc.Chain, proposalID uint64) error {
|
|
// poll for the query for the entire voting period to see if the proposal has passed.
|
|
return test.WaitForCondition(testvalues.VotingPeriod, 10*time.Second, func() (bool, error) {
|
|
proposalResp, err := query.GRPCQuery[govtypesv1beta1.QueryProposalResponse](ctx, chain, &govtypesv1beta1.QueryProposalRequest{
|
|
ProposalId: proposalID,
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
proposal := proposalResp.Proposal
|
|
return proposal.Status == govtypesv1beta1.StatusPassed, nil
|
|
})
|
|
}
|
|
|
|
// ExecuteGovV1Beta1Proposal submits a v1beta1 governance proposal using the provided content.
|
|
func (s *E2ETestSuite) ExecuteGovV1Beta1Proposal(ctx context.Context, chain ibc.Chain, user ibc.Wallet, content govtypesv1beta1.Content) sdk.TxResponse {
|
|
sender, err := sdk.AccAddressFromBech32(user.FormattedAddress())
|
|
s.Require().NoError(err)
|
|
|
|
msgSubmitProposal, err := govtypesv1beta1.NewMsgSubmitProposal(content, sdk.NewCoins(sdk.NewCoin(chain.Config().Denom, govtypesv1beta1.DefaultMinDepositTokens)), sender)
|
|
s.Require().NoError(err)
|
|
|
|
return s.BroadcastMessages(ctx, chain, user, msgSubmitProposal)
|
|
}
|
|
|
|
// Transfer broadcasts a MsgTransfer message.
|
|
func (s *E2ETestSuite) Transfer(ctx context.Context, chain ibc.Chain, user ibc.Wallet,
|
|
portID, channelID string, token sdk.Coin, sender, receiver string,
|
|
timeoutHeight clienttypes.Height, timeoutTimestamp uint64,
|
|
memo string,
|
|
) sdk.TxResponse {
|
|
channel, err := query.Channel(ctx, chain, portID, channelID)
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(channel)
|
|
|
|
msg := GetMsgTransfer(portID, channelID, channel.Version, token, sender, receiver, timeoutHeight, timeoutTimestamp, memo)
|
|
|
|
return s.BroadcastMessages(ctx, chain, user, msg)
|
|
}
|
|
|
|
// QueryTxsByEvents runs the QueryTxsByEvents command on the given chain.
|
|
// https://git.cw.tr/mukan-network/mukan-sdk/blob/65ab2530cc654fd9e252b124ed24cbaa18023b2b/x/auth/client/cli/query.go#L33
|
|
func (*E2ETestSuite) QueryTxsByEvents(
|
|
ctx context.Context, chain ibc.Chain,
|
|
page, limit int, queryReq, orderBy string,
|
|
) (*txtypes.GetTxsEventResponse, error) {
|
|
cosmosChain, ok := chain.(*cosmos.CosmosChain)
|
|
if !ok {
|
|
return nil, errors.New("QueryTxsByEvents must be passed a cosmos.CosmosChain")
|
|
}
|
|
|
|
req := &txtypes.GetTxsEventRequest{
|
|
Page: uint64(page),
|
|
Limit: uint64(limit),
|
|
Query: queryReq,
|
|
}
|
|
|
|
if !testvalues.TransactionEventQueryFeatureReleases.IsSupported(chain.Config().Images[0].Version) {
|
|
req.Events = []string{queryReq}
|
|
req.Query = ""
|
|
}
|
|
|
|
res, err := query.GRPCQuery[txtypes.GetTxsEventResponse](ctx, cosmosChain, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// ExtractValueFromEvents extracts the value of an attribute from a list of events.
|
|
// If the attribute is not found, the function returns an empty string and false.
|
|
// If the attribute is found, the function returns the value and true.
|
|
func (*E2ETestSuite) ExtractValueFromEvents(events []abci.Event, eventType, attrKey string) (string, bool) {
|
|
for _, event := range events {
|
|
if event.Type != eventType {
|
|
continue
|
|
}
|
|
|
|
for _, attr := range event.Attributes {
|
|
if attr.Key != attrKey {
|
|
continue
|
|
}
|
|
|
|
return attr.Value, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|