mukan-ibc/e2e/testsuite/tx.go
Mukan Erkin Törük 88dd97a9f8
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
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:22 +03:00

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
}