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 }