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
740 lines
30 KiB
Go
740 lines
30 KiB
Go
package testsuite
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
interchaintest "github.com/cosmos/interchaintest/v10"
|
|
"github.com/cosmos/interchaintest/v10/chain/cosmos"
|
|
"github.com/cosmos/interchaintest/v10/ibc"
|
|
"github.com/cosmos/interchaintest/v10/testreporter"
|
|
test "github.com/cosmos/interchaintest/v10/testutil"
|
|
mobycli "github.com/moby/moby/client"
|
|
testifysuite "github.com/stretchr/testify/suite"
|
|
"go.uber.org/zap"
|
|
|
|
sdkmath "cosmossdk.io/math"
|
|
|
|
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
|
|
banktypes "git.cw.tr/mukan-network/mukan-sdk/x/bank/types"
|
|
|
|
"github.com/cosmos/ibc-go/e2e/internal/directories"
|
|
"github.com/cosmos/ibc-go/e2e/relayer"
|
|
"github.com/cosmos/ibc-go/e2e/testsuite/diagnostics"
|
|
"github.com/cosmos/ibc-go/e2e/testsuite/query"
|
|
transfertypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/transfer/types"
|
|
clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
|
|
channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
|
|
)
|
|
|
|
const (
|
|
// ChainARelayerName is the name given to the relayer wallet on ChainA
|
|
ChainARelayerName = "rlyA"
|
|
// ChainBRelayerName is the name given to the relayer wallet on ChainB
|
|
ChainBRelayerName = "rlyB"
|
|
// DefaultGasValue is the default gas value used to configure tx.Factory
|
|
DefaultGasValue = 500_000_0000
|
|
)
|
|
|
|
// E2ETestSuite has methods and functionality which can be shared among all test suites.
|
|
type E2ETestSuite struct {
|
|
testifysuite.Suite
|
|
|
|
// proposalIDs keeps track of the active proposal ID for each chain.
|
|
proposalIDs map[string]uint64
|
|
|
|
// chains is a list of chains that are created for the test suite.
|
|
// each test suite has a single slice of chains that are used for all individual test
|
|
// cases.
|
|
chains []ibc.Chain
|
|
relayerWallets relayer.Map
|
|
logger *zap.Logger
|
|
DockerClient *mobycli.Client
|
|
network string
|
|
|
|
// pathNameIndex is the latest index to be used for generating chains
|
|
pathNameIndex int64
|
|
|
|
// testSuiteName is the name of the test suite, used to store chains under the test suite name.
|
|
testSuiteName string
|
|
testPaths map[string][]string
|
|
channels map[string]map[ibc.Chain][]ibc.ChannelOutput
|
|
|
|
// channelLock ensures concurrent tests are not creating and accessing channels as the same time.
|
|
channelLock sync.Mutex
|
|
// relayerLock ensures concurrent tests are not accessing the pool of relayers as the same time.
|
|
relayerLock sync.Mutex
|
|
// relayerPool is a pool of relayers that can be used in tests.
|
|
relayerPool []ibc.Relayer
|
|
// testRelayerMap is a map of test suite names to relayers that are used in the test suite.
|
|
// this is used as a cache after a relayer has been assigned to a test suite.
|
|
testRelayerMap map[string]ibc.Relayer
|
|
}
|
|
|
|
// initState populates variables that are used across the test suite.
|
|
// note: this should be called only from SetupSuite.
|
|
func (s *E2ETestSuite) initState() {
|
|
s.initDockerClient()
|
|
s.proposalIDs = map[string]uint64{}
|
|
s.testPaths = make(map[string][]string)
|
|
s.channels = make(map[string]map[ibc.Chain][]ibc.ChannelOutput)
|
|
s.relayerPool = []ibc.Relayer{}
|
|
s.testRelayerMap = make(map[string]ibc.Relayer)
|
|
s.relayerWallets = make(relayer.Map)
|
|
|
|
// testSuiteName gets populated in the context of SetupSuite and stored as s.T().Name()
|
|
// will return the name of the suite and test when called from SetupTest or within the body of tests.
|
|
// the chains will be stored under the test suite name, so we need to store this for future use.
|
|
s.testSuiteName = s.T().Name()
|
|
}
|
|
|
|
// initDockerClient creates a docker client and populates the network to be used for the test.
|
|
func (s *E2ETestSuite) initDockerClient() {
|
|
client, network := interchaintest.DockerSetup(s.T())
|
|
s.logger = zap.NewExample()
|
|
s.DockerClient = client
|
|
s.network = network
|
|
}
|
|
|
|
// configureGenesisDebugExport sets, if needed, env variables to enable exporting of Genesis debug files.
|
|
func (s *E2ETestSuite) configureGenesisDebugExport() {
|
|
tc := LoadConfig()
|
|
t := s.T()
|
|
cfg := tc.DebugConfig.GenesisDebug
|
|
if !cfg.DumpGenesisDebugInfo {
|
|
return
|
|
}
|
|
|
|
// Set the export path.
|
|
exportPath := cfg.ExportFilePath
|
|
|
|
// If no path is provided, use the default (e2e/diagnostics/genesis.json).
|
|
if exportPath == "" {
|
|
e2eDir, err := directories.E2E()
|
|
s.Require().NoError(err, "can't get e2edir")
|
|
exportPath = path.Join(e2eDir, directories.DefaultGenesisExportPath)
|
|
}
|
|
|
|
if !path.IsAbs(exportPath) {
|
|
wd, err := os.Getwd()
|
|
s.Require().NoError(err, "can't get working directory")
|
|
exportPath = path.Join(wd, exportPath)
|
|
}
|
|
|
|
// These environment variables are set by interchaintest at runtime.
|
|
// Reference: https://github.com/cosmos/interchaintest/blob/main/chain/cosmos/cosmos_chain.go
|
|
t.Setenv("EXPORT_GENESIS_FILE_PATH", exportPath)
|
|
|
|
chainName := tc.GetGenesisChainName()
|
|
chainIdx, err := tc.GetChainIndex(chainName)
|
|
s.Require().NoError(err)
|
|
|
|
// interchaintest adds a numeric suffix to the chain name, so we do the same.
|
|
genesisChainName := fmt.Sprintf("%s-%d", chainName, chainIdx+1)
|
|
t.Setenv("EXPORT_GENESIS_CHAIN", genesisChainName)
|
|
}
|
|
|
|
// initializeRelayerPool pre-loads the relayer pool with n relayers.
|
|
// this is a workaround due to the restriction on relayer creation during the test
|
|
// If/when interchaintest supports relayer creation during tests, this can be made lazy per-test.
|
|
func (s *E2ETestSuite) initializeRelayerPool(n int) []ibc.Relayer {
|
|
var relayers []ibc.Relayer
|
|
for range n {
|
|
relayers = append(relayers, relayer.New(s.T(), *LoadConfig().GetActiveRelayerConfig(), s.logger, s.DockerClient, s.network))
|
|
}
|
|
return relayers
|
|
}
|
|
|
|
// SetupChains creates the chains for the test suite, and also a relayer that is wired up to establish
|
|
// connections and channels between the chains.
|
|
func (s *E2ETestSuite) SetupChains(ctx context.Context, chainCount int, channelOptionsModifier ChainOptionModifier, chainSpecOpts ...ChainOptionConfiguration) {
|
|
s.T().Logf("Setting up %d chains: %s", chainCount, s.T().Name())
|
|
|
|
if LoadConfig().DebugConfig.KeepContainers {
|
|
s.Require().NoError(os.Setenv(KeepContainersEnv, "true"))
|
|
}
|
|
|
|
s.initState()
|
|
s.configureGenesisDebugExport()
|
|
|
|
chainOptions, err := DefaultChainOptions(chainCount)
|
|
s.Require().NoError(err)
|
|
|
|
for _, opt := range chainSpecOpts {
|
|
opt(&chainOptions)
|
|
}
|
|
|
|
specCount := len(chainOptions.ChainSpecs)
|
|
s.Require().GreaterOrEqualf(specCount, chainCount, "wants to create %d chains, but DefaultChainOptions has %d configurations", chainCount, specCount)
|
|
|
|
s.chains = s.createChains(chainCount, chainOptions)
|
|
|
|
s.relayerPool = s.initializeRelayerPool(chainOptions.RelayerCount)
|
|
|
|
ic := s.newInterchain(s.relayerPool, s.chains, channelOptionsModifier)
|
|
|
|
buildOpts := interchaintest.InterchainBuildOptions{
|
|
TestName: s.T().Name(),
|
|
Client: s.DockerClient,
|
|
NetworkID: s.network,
|
|
// we skip path creation because we are just creating the chains and not connections/channels
|
|
SkipPathCreation: true,
|
|
}
|
|
|
|
s.Require().NoError(ic.Build(ctx, s.GetRelayerExecReporter(), buildOpts))
|
|
|
|
// setup query paths for GRPC queries:
|
|
for _, chain := range s.chains {
|
|
s.Require().NoError(query.PopulateQueryReqToPath(ctx, chain))
|
|
}
|
|
}
|
|
|
|
// CreateDefaultPaths creates a path between the chains using the default client and channel options.
|
|
// this should be called as the setup function in most tests if no additional options are required.
|
|
func (s *E2ETestSuite) CreateDefaultPaths(testName string) ibc.Relayer {
|
|
return s.CreatePaths(ibc.DefaultClientOpts(), DefaultChannelOpts(s.GetAllChains()), testName)
|
|
}
|
|
|
|
// CreatePaths creates paths between the chains using the provided client and channel options.
|
|
// The paths are created such that ChainA is connected to ChainB, ChainB is connected to ChainC etc.
|
|
func (s *E2ETestSuite) CreatePaths(clientOpts ibc.CreateClientOptions, channelOpts ibc.CreateChannelOptions, testName string) ibc.Relayer {
|
|
s.T().Logf("Setting up path for: %s", testName)
|
|
|
|
if s.channels[testName] == nil {
|
|
s.channels[testName] = make(map[ibc.Chain][]ibc.ChannelOutput)
|
|
}
|
|
|
|
r := s.GetRelayerForTest(testName)
|
|
|
|
ctx := context.TODO()
|
|
allChains := s.GetAllChains()
|
|
for i := range len(allChains) - 1 {
|
|
chainA, chainB := allChains[i], allChains[i+1]
|
|
s.CreatePath(ctx, r, chainA, chainB, clientOpts, channelOpts, testName)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// CreatePath creates a path between chainA and chainB using the provided client and channel options.
|
|
func (s *E2ETestSuite) CreatePath(
|
|
ctx context.Context,
|
|
r ibc.Relayer,
|
|
chainA ibc.Chain,
|
|
chainB ibc.Chain,
|
|
clientOpts ibc.CreateClientOptions,
|
|
channelOpts ibc.CreateChannelOptions,
|
|
testName string,
|
|
) (chainAChannel ibc.ChannelOutput, chainBChannel ibc.ChannelOutput) {
|
|
pathName := s.generatePathName()
|
|
s.testPaths[testName] = append(s.testPaths[testName], pathName)
|
|
|
|
s.T().Logf("establishing path between %s and %s on path %s", chainA.Config().ChainID, chainB.Config().ChainID, pathName)
|
|
|
|
err := r.GeneratePath(ctx, s.GetRelayerExecReporter(), chainA.Config().ChainID, chainB.Config().ChainID, pathName)
|
|
s.Require().NoError(err)
|
|
|
|
// Create new clients
|
|
err = r.CreateClients(ctx, s.GetRelayerExecReporter(), pathName, clientOpts)
|
|
s.Require().NoError(err)
|
|
err = test.WaitForBlocks(ctx, 1, chainA, chainB)
|
|
s.Require().NoError(err)
|
|
|
|
err = r.CreateConnections(ctx, s.GetRelayerExecReporter(), pathName)
|
|
s.Require().NoError(err)
|
|
err = test.WaitForBlocks(ctx, 1, chainA, chainB)
|
|
s.Require().NoError(err)
|
|
|
|
s.createChannelWithLock(ctx, r, pathName, testName, channelOpts, chainA, chainB)
|
|
|
|
aChannels := s.channels[testName][chainA]
|
|
bChannels := s.channels[testName][chainB]
|
|
|
|
return aChannels[len(aChannels)-1], bChannels[len(bChannels)-1]
|
|
}
|
|
|
|
// createChannelWithLock creates a channel between the two provided chains for the given test name. This applies a lock
|
|
// to ensure that the channels that are created are correctly mapped to the test that created them.
|
|
func (s *E2ETestSuite) createChannelWithLock(ctx context.Context, r ibc.Relayer, pathName, testName string, channelOpts ibc.CreateChannelOptions, chainA, chainB ibc.Chain) {
|
|
// NOTE: we need to lock the creation of channels and applying of packet filters, as if we don't, the result
|
|
// of `r.GetChannels` may return channels created by other relayers in different tests.
|
|
s.channelLock.Lock()
|
|
defer s.channelLock.Unlock()
|
|
|
|
err := r.CreateChannel(ctx, s.GetRelayerExecReporter(), pathName, channelOpts)
|
|
s.Require().NoError(err)
|
|
err = test.WaitForBlocks(ctx, 1, chainA, chainB)
|
|
s.Require().NoError(err)
|
|
|
|
for _, c := range []ibc.Chain{chainA, chainB} {
|
|
channels, err := r.GetChannels(ctx, s.GetRelayerExecReporter(), c.Config().ChainID)
|
|
s.Require().NoError(err)
|
|
|
|
if _, ok := s.channels[testName][c]; !ok {
|
|
s.channels[testName][c] = []ibc.ChannelOutput{}
|
|
}
|
|
|
|
// keep track of channels associated with a given chain for access within the tests.
|
|
// only the most recent channel is relevant.
|
|
s.channels[testName][c] = append(s.channels[testName][c], getLatestChannel(channels))
|
|
|
|
err = relayer.ApplyPacketFilter(ctx, s.T(), r, c.Config().ChainID, s.channels[testName][c])
|
|
s.Require().NoError(err, "failed to watch port and channel on chain: %s", c.Config().ChainID)
|
|
}
|
|
}
|
|
|
|
// getLatestChannel returns the latest channel from the list of channels.
|
|
func getLatestChannel(channels []ibc.ChannelOutput) ibc.ChannelOutput {
|
|
return slices.MaxFunc(channels, func(a, b ibc.ChannelOutput) int {
|
|
seqA, _ := channeltypes.ParseChannelSequence(a.ChannelID)
|
|
seqB, _ := channeltypes.ParseChannelSequence(b.ChannelID)
|
|
return int(seqA - seqB)
|
|
})
|
|
}
|
|
|
|
// GetChainAChannelForTest returns the ibc.ChannelOutput for the current test.
|
|
// this defaults to the first entry in the list, and will be what is needed in the case of
|
|
// a single channel test.
|
|
func (s *E2ETestSuite) GetChainAChannelForTest(testName string) ibc.ChannelOutput {
|
|
return s.GetChannelsForTest(s.GetAllChains()[0], testName)[0]
|
|
}
|
|
|
|
// GetChannelsForTest returns all channels for the specified test.
|
|
func (s *E2ETestSuite) GetChannelsForTest(chain ibc.Chain, testName string) []ibc.ChannelOutput {
|
|
channels, ok := s.channels[testName][chain]
|
|
s.Require().True(ok, "channel not found for test %s", testName)
|
|
return channels
|
|
}
|
|
|
|
// GetRelayerForTest returns the relayer for the current test from the available pool of relayers.
|
|
// once a relayer has been returned to a test, it is cached and will be reused for the duration of the test.
|
|
func (s *E2ETestSuite) GetRelayerForTest(testName string) ibc.Relayer {
|
|
s.relayerLock.Lock()
|
|
defer s.relayerLock.Unlock()
|
|
|
|
if r, ok := s.testRelayerMap[testName]; ok {
|
|
s.T().Logf("relayer already created for test: %s", testName)
|
|
return r
|
|
}
|
|
|
|
if len(s.relayerPool) == 0 {
|
|
panic(errors.New("relayer pool is empty"))
|
|
}
|
|
|
|
r := s.relayerPool[0]
|
|
|
|
// remove the relayer from the pool
|
|
s.relayerPool = s.relayerPool[1:]
|
|
|
|
s.testRelayerMap[testName] = r
|
|
|
|
return r
|
|
}
|
|
|
|
// GetRelayerUsers returns two ibc.Wallet instances which can be used for the relayer users
|
|
// on the two chains.
|
|
func (s *E2ETestSuite) GetRelayerUsers(ctx context.Context, testName string) (ibc.Wallet, ibc.Wallet) {
|
|
chains := s.GetAllChains()
|
|
chainA, chainB := chains[0], chains[1]
|
|
|
|
rlyAName := fmt.Sprintf("%s-%s", ChainARelayerName, testName)
|
|
rlyBName := fmt.Sprintf("%s-%s", ChainBRelayerName, testName)
|
|
|
|
chainAAccountBytes, err := chainA.GetAddress(ctx, rlyAName)
|
|
s.Require().NoError(err)
|
|
|
|
chainBAccountBytes, err := chainB.GetAddress(ctx, rlyBName)
|
|
s.Require().NoError(err)
|
|
|
|
chainARelayerUser := cosmos.NewWallet(rlyAName, chainAAccountBytes, "", chainA.Config())
|
|
chainBRelayerUser := cosmos.NewWallet(rlyBName, chainBAccountBytes, "", chainB.Config())
|
|
|
|
s.relayerWallets.AddRelayer(testName, chainARelayerUser)
|
|
s.relayerWallets.AddRelayer(testName, chainBRelayerUser)
|
|
|
|
return chainARelayerUser, chainBRelayerUser
|
|
}
|
|
|
|
// ChainOptionModifier is a function which accepts 2 chains as inputs, and returns a channel creation modifier function
|
|
// in order to conditionally modify the channel options based on the chains being used.
|
|
type ChainOptionModifier func(chainA, chainB ibc.Chain) func(options *ibc.CreateChannelOptions)
|
|
|
|
// newInterchain constructs a new interchain instance that creates channels between the chains.
|
|
func (s *E2ETestSuite) newInterchain(relayers []ibc.Relayer, chains []ibc.Chain, modificationProvider ChainOptionModifier) *interchaintest.Interchain {
|
|
ic := interchaintest.NewInterchain()
|
|
for _, chain := range chains {
|
|
ic.AddChain(chain)
|
|
}
|
|
|
|
for i, r := range relayers {
|
|
ic.AddRelayer(r, fmt.Sprintf("r-%d", i))
|
|
}
|
|
|
|
// iterate through all chains, and create links such that there is a channel between
|
|
// - chainA and chainB
|
|
// - chainB and chainC
|
|
// - chainC and chainD etc
|
|
for i := range len(chains) - 1 {
|
|
pathName := s.generatePathName()
|
|
channelOpts := DefaultChannelOpts(chains)
|
|
chain1, chain2 := chains[i], chains[i+1]
|
|
|
|
if modificationProvider != nil {
|
|
// make a modification to the channel options based on the chains which are being used.
|
|
modificationFn := modificationProvider(chain1, chain2)
|
|
modificationFn(&channelOpts)
|
|
}
|
|
|
|
for _, r := range relayers {
|
|
ic.AddLink(interchaintest.InterchainLink{
|
|
Chain1: chains[i],
|
|
Chain2: chains[i+1],
|
|
Relayer: r,
|
|
Path: pathName,
|
|
CreateChannelOpts: channelOpts,
|
|
})
|
|
}
|
|
}
|
|
|
|
return ic
|
|
}
|
|
|
|
// generatePathName generates the path name using the test suites name
|
|
func (s *E2ETestSuite) generatePathName() string {
|
|
pathName := GetPathName(s.pathNameIndex)
|
|
s.pathNameIndex++
|
|
return pathName
|
|
}
|
|
|
|
func (s *E2ETestSuite) GetPaths(testName string) []string {
|
|
paths, ok := s.testPaths[testName]
|
|
s.Require().True(ok, "paths not found for test %s", testName)
|
|
return paths
|
|
}
|
|
|
|
// GetPathName returns the name of a path at a specific index. This can be used in tests
|
|
// when the path name is required.
|
|
func GetPathName(idx int64) string {
|
|
pathName := fmt.Sprintf("path-%d", idx)
|
|
return strings.ReplaceAll(pathName, "/", "-")
|
|
}
|
|
|
|
// generatePath generates the path name using the test suites name. The indices provided specify which chains should be
|
|
// used. E.g. to generate a path between chain A and B, you would use 0 and 1, to specify between A and C, you would
|
|
// use 0 and 2 etc.
|
|
func (s *E2ETestSuite) generatePath(ctx context.Context, ibcrelayer ibc.Relayer, chainAIdx, chainBIdx int) string {
|
|
chains := s.GetAllChains()
|
|
chainA, chainB := chains[chainAIdx], chains[chainBIdx]
|
|
chainAID := chainA.Config().ChainID
|
|
chainBID := chainB.Config().ChainID
|
|
|
|
pathName := s.generatePathName()
|
|
|
|
err := ibcrelayer.GeneratePath(ctx, s.GetRelayerExecReporter(), chainAID, chainBID, pathName)
|
|
s.Require().NoError(err)
|
|
|
|
return pathName
|
|
}
|
|
|
|
// SetupClients creates clients on chainA and chainB using the provided create client options
|
|
func (s *E2ETestSuite) SetupClients(ctx context.Context, ibcrelayer ibc.Relayer, opts ibc.CreateClientOptions) {
|
|
pathName := s.generatePath(ctx, ibcrelayer, 0, 1)
|
|
err := ibcrelayer.CreateClients(ctx, s.GetRelayerExecReporter(), pathName, opts)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// UpdateClients updates clients on chainA and chainB
|
|
func (s *E2ETestSuite) UpdateClients(ctx context.Context, ibcrelayer ibc.Relayer, pathName string) {
|
|
err := ibcrelayer.UpdateClients(ctx, s.GetRelayerExecReporter(), pathName)
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// GetChains returns two chains that can be used in a test. The pair returned
|
|
// is unique to the current test being run. Note: this function does not create containers.
|
|
func (s *E2ETestSuite) GetChains() (ibc.Chain, ibc.Chain) {
|
|
chains := s.GetAllChains()
|
|
return chains[0], chains[1]
|
|
}
|
|
|
|
// GetAllChains returns all chains that can be used in a test. The chains returned
|
|
// are unique to the current test being run. Note: this function does not create containers.
|
|
func (s *E2ETestSuite) GetAllChains() []ibc.Chain {
|
|
// chains are stored on a per test suite level
|
|
chains := s.chains
|
|
s.Require().NotEmpty(chains, "chains not found for test %s", s.testSuiteName)
|
|
return chains
|
|
}
|
|
|
|
// GetRelayerWallets returns the ibcrelayer wallets associated with the chains.
|
|
func (s *E2ETestSuite) GetRelayerWallets(ibcrelayer ibc.Relayer) (ibc.Wallet, ibc.Wallet, error) {
|
|
chains := s.GetAllChains()
|
|
chainA, chainB := chains[0], chains[1]
|
|
chainARelayerWallet, ok := ibcrelayer.GetWallet(chainA.Config().ChainID)
|
|
if !ok {
|
|
return nil, nil, errors.New("unable to find chain A relayer wallet")
|
|
}
|
|
|
|
chainBRelayerWallet, ok := ibcrelayer.GetWallet(chainB.Config().ChainID)
|
|
if !ok {
|
|
return nil, nil, errors.New("unable to find chain B relayer wallet")
|
|
}
|
|
return chainARelayerWallet, chainBRelayerWallet, nil
|
|
}
|
|
|
|
// RecoverRelayerWallets adds the corresponding ibcrelayer address to the keychain of the chain.
|
|
// This is useful if commands executed on the chains expect the relayer information to present in the keychain.
|
|
func (s *E2ETestSuite) RecoverRelayerWallets(ctx context.Context, ibcrelayer ibc.Relayer, testName string) (ibc.Wallet, ibc.Wallet, error) {
|
|
chainARelayerWallet, chainBRelayerWallet, err := s.GetRelayerWallets(ibcrelayer)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
chains := s.GetAllChains()
|
|
chainA, chainB := chains[0], chains[1]
|
|
|
|
rlyAName := fmt.Sprintf("%s-%s", ChainARelayerName, testName)
|
|
rlyBName := fmt.Sprintf("%s-%s", ChainBRelayerName, testName)
|
|
|
|
if err := chainA.RecoverKey(ctx, rlyAName, chainARelayerWallet.Mnemonic()); err != nil {
|
|
return nil, nil, fmt.Errorf("could not recover relayer wallet on chain A: %s", err)
|
|
}
|
|
if err := chainB.RecoverKey(ctx, rlyBName, chainBRelayerWallet.Mnemonic()); err != nil {
|
|
return nil, nil, fmt.Errorf("could not recover relayer wallet on chain B: %s", err)
|
|
}
|
|
return chainARelayerWallet, chainBRelayerWallet, nil
|
|
}
|
|
|
|
// StartRelayer starts the given ibcrelayer.
|
|
func (s *E2ETestSuite) StartRelayer(r ibc.Relayer, testName string) {
|
|
s.Require().NoError(r.StartRelayer(context.TODO(), s.GetRelayerExecReporter(), s.GetPaths(testName)...), "failed to start relayer")
|
|
|
|
chains := s.GetAllChains()
|
|
var chainHeighters []test.ChainHeighter
|
|
for _, c := range chains {
|
|
chainHeighters = append(chainHeighters, c)
|
|
}
|
|
|
|
// wait for every chain to produce some blocks before using the relayer.
|
|
s.Require().NoError(test.WaitForBlocks(context.TODO(), 10, chainHeighters...), "failed to wait for blocks")
|
|
}
|
|
|
|
// StopRelayer stops the given ibcrelayer.
|
|
func (s *E2ETestSuite) StopRelayer(ctx context.Context, ibcrelayer ibc.Relayer) {
|
|
err := ibcrelayer.StopRelayer(ctx, s.GetRelayerExecReporter())
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// RestartRelayer restarts the given relayer.
|
|
func (s *E2ETestSuite) RestartRelayer(ctx context.Context, ibcrelayer ibc.Relayer, testName string) {
|
|
s.StopRelayer(ctx, ibcrelayer)
|
|
s.StartRelayer(ibcrelayer, testName)
|
|
}
|
|
|
|
// CreateUserOnChainA creates a user with the given amount of funds on chain A.
|
|
func (s *E2ETestSuite) CreateUserOnChainA(ctx context.Context, amount int64) ibc.Wallet {
|
|
return s.createWalletOnChainIndex(ctx, amount, 0)
|
|
}
|
|
|
|
// CreateUserOnChainB creates a user with the given amount of funds on chain B.
|
|
func (s *E2ETestSuite) CreateUserOnChainB(ctx context.Context, amount int64) ibc.Wallet {
|
|
return s.createWalletOnChainIndex(ctx, amount, 1)
|
|
}
|
|
|
|
// CreateUserOnChainC creates a user with the given amount of funds on chain C.
|
|
func (s *E2ETestSuite) CreateUserOnChainC(ctx context.Context, amount int64) ibc.Wallet {
|
|
return s.createWalletOnChainIndex(ctx, amount, 2)
|
|
}
|
|
|
|
// createWalletOnChainIndex creates a wallet with the given amount of funds on the chain of the given index.
|
|
func (s *E2ETestSuite) createWalletOnChainIndex(ctx context.Context, amount, chainIndex int64) ibc.Wallet {
|
|
chain := s.GetAllChains()[chainIndex]
|
|
wallet := interchaintest.GetAndFundTestUsers(s.T(), ctx, strings.ReplaceAll(s.T().Name(), " ", "-"), sdkmath.NewInt(amount), chain)[0]
|
|
// note the GetAndFundTestUsers requires the caller to wait for some blocks before the funds are accessible.
|
|
s.Require().NoError(test.WaitForBlocks(ctx, 2, chain))
|
|
return wallet
|
|
}
|
|
|
|
// GetChainANativeBalance gets the balance of a given user on chain A.
|
|
func (s *E2ETestSuite) GetChainANativeBalance(ctx context.Context, user ibc.Wallet) (int64, error) {
|
|
chainA := s.GetAllChains()[0]
|
|
return GetChainBalanceForDenom(ctx, chainA, chainA.Config().Denom, user)
|
|
}
|
|
|
|
// GetChainBNativeBalance gets the balance of a given user on chain B.
|
|
func (s *E2ETestSuite) GetChainBNativeBalance(ctx context.Context, user ibc.Wallet) (int64, error) {
|
|
chainB := s.GetAllChains()[1]
|
|
return GetChainBalanceForDenom(ctx, chainB, chainB.Config().Denom, user)
|
|
}
|
|
|
|
// GetChainBalanceForDenom returns the balance for a given denom given a chain.
|
|
func GetChainBalanceForDenom(ctx context.Context, chain ibc.Chain, denom string, user ibc.Wallet) (int64, error) {
|
|
balanceResp, err := query.GRPCQuery[banktypes.QueryBalanceResponse](ctx, chain, &banktypes.QueryBalanceRequest{
|
|
Address: user.FormattedAddress(),
|
|
Denom: denom,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return balanceResp.Balance.Amount.Int64(), nil
|
|
}
|
|
|
|
// AssertPacketRelayed asserts that the packet commitment does not exist on the sending chain.
|
|
// The packet commitment will be deleted upon a packet acknowledgement or timeout.
|
|
func (s *E2ETestSuite) AssertPacketRelayed(ctx context.Context, chain ibc.Chain, portID, channelID string, sequence uint64) {
|
|
_, err := query.GRPCQuery[channeltypes.QueryPacketCommitmentResponse](ctx, chain, &channeltypes.QueryPacketCommitmentRequest{
|
|
PortId: portID,
|
|
ChannelId: channelID,
|
|
Sequence: sequence,
|
|
})
|
|
s.Require().ErrorContains(err, "packet commitment hash not found")
|
|
}
|
|
|
|
// AssertPacketAcknowledged asserts that the packet has been acknowledged on the specified chain.
|
|
func (s *E2ETestSuite) AssertPacketAcknowledged(ctx context.Context, chain ibc.Chain, portID, channelID string, sequence uint64) {
|
|
_, err := query.GRPCQuery[channeltypes.QueryPacketAcknowledgementResponse](ctx, chain, &channeltypes.QueryPacketAcknowledgementRequest{
|
|
PortId: portID,
|
|
ChannelId: channelID,
|
|
Sequence: sequence,
|
|
})
|
|
s.Require().NoError(err)
|
|
}
|
|
|
|
// AssertHumanReadableDenom asserts that a human readable denom is present for a given chain.
|
|
func (s *E2ETestSuite) AssertHumanReadableDenom(ctx context.Context, chain ibc.Chain, counterpartyNativeDenom string, counterpartyChannel ibc.ChannelOutput) {
|
|
chainIBCDenom := GetIBCToken(counterpartyNativeDenom, counterpartyChannel.Counterparty.PortID, counterpartyChannel.Counterparty.ChannelID)
|
|
|
|
denomMetadataResp, err := query.GRPCQuery[banktypes.QueryDenomMetadataResponse](ctx, chain, &banktypes.QueryDenomMetadataRequest{
|
|
Denom: chainIBCDenom.IBCDenom(),
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
denomMetadata := denomMetadataResp.Metadata
|
|
|
|
s.Require().Equal(chainIBCDenom.IBCDenom(), denomMetadata.Base, "denom metadata base does not match expected %s: got %s", chainIBCDenom.IBCDenom(), denomMetadata.Base)
|
|
expectedName := fmt.Sprintf("%s/%s/%s IBC token", counterpartyChannel.Counterparty.PortID, counterpartyChannel.Counterparty.ChannelID, counterpartyNativeDenom)
|
|
s.Require().Equal(expectedName, denomMetadata.Name, "denom metadata name does not match expected %s: got %s", expectedName, denomMetadata.Name)
|
|
expectedDisplay := fmt.Sprintf("%s/%s/%s", counterpartyChannel.Counterparty.PortID, counterpartyChannel.Counterparty.ChannelID, counterpartyNativeDenom)
|
|
s.Require().Equal(expectedDisplay, denomMetadata.Display, "denom metadata display does not match expected %s: got %s", expectedDisplay, denomMetadata.Display)
|
|
s.Require().Equal(strings.ToUpper(counterpartyNativeDenom), denomMetadata.Symbol, "denom metadata symbol does not match expected %s: got %s", strings.ToUpper(counterpartyNativeDenom), denomMetadata.Symbol)
|
|
}
|
|
|
|
// createChains creates two separate chains in docker containers.
|
|
// test and can be retrieved with GetChains.
|
|
func (s *E2ETestSuite) createChains(chainCount int, chainOptions ChainOptions) []ibc.Chain {
|
|
t := s.T()
|
|
s.Require().GreaterOrEqualf(len(chainOptions.ChainSpecs), chainCount, "len(chainOptions.ChainSpecs): %d < chainCount: %d", len(chainOptions.ChainSpecs), chainCount)
|
|
|
|
cf := interchaintest.NewBuiltinChainFactory(s.logger, chainOptions.ChainSpecs[:chainCount]) // Take the first N specs
|
|
|
|
// this is intentionally called after the interchaintest.DockerSetup function. The above function registers a
|
|
// cleanup task which deletes all containers. By registering a cleanup function afterwards, it is executed first
|
|
// this allows us to process the logs before the containers are removed.
|
|
t.Cleanup(func() {
|
|
dumpLogs := LoadConfig().DebugConfig.DumpLogs
|
|
var chainNames []string
|
|
for _, chain := range chainOptions.ChainSpecs {
|
|
chainNames = append(chainNames, chain.Name)
|
|
}
|
|
diagnostics.Collect(t, s.DockerClient, dumpLogs, s.testSuiteName, chainNames...)
|
|
})
|
|
|
|
chains, err := cf.Chains(t.Name())
|
|
s.Require().NoError(err)
|
|
|
|
// initialise proposal ids for all chains.
|
|
for _, chain := range chains {
|
|
s.proposalIDs[chain.Config().ChainID] = 1
|
|
}
|
|
|
|
return chains
|
|
}
|
|
|
|
// GetRelayerExecReporter returns a testreporter.RelayerExecReporter instances
|
|
// using the current test's testing.T.
|
|
func (s *E2ETestSuite) GetRelayerExecReporter() *testreporter.RelayerExecReporter {
|
|
rep := testreporter.NewNopReporter()
|
|
return rep.RelayerExecReporter(s.T())
|
|
}
|
|
|
|
// TransferChannelOptions configures both of the chains to have non-incentivized transfer channels.
|
|
func (*E2ETestSuite) TransferChannelOptions() ibc.CreateChannelOptions {
|
|
opts := ibc.DefaultChannelOpts()
|
|
opts.Version = transfertypes.V1
|
|
return opts
|
|
}
|
|
|
|
// GetTimeoutHeight returns a timeout height of 1000 blocks above the current block height.
|
|
// This function should be used when the timeout is never expected to be reached
|
|
func (s *E2ETestSuite) GetTimeoutHeight(ctx context.Context, chain ibc.Chain) clienttypes.Height {
|
|
height, err := chain.Height(ctx)
|
|
s.Require().NoError(err)
|
|
return clienttypes.NewHeight(clienttypes.ParseChainID(chain.Config().ChainID), uint64(height)+1000)
|
|
}
|
|
|
|
// GetIBCToken returns the denomination of the full token denom sent to the receiving channel
|
|
func GetIBCToken(fullTokenDenom string, portID, channelID string) transfertypes.Denom {
|
|
return transfertypes.ExtractDenomFromPath(fmt.Sprintf("%s/%s/%s", portID, channelID, fullTokenDenom))
|
|
}
|
|
|
|
// getValidatorsAndFullNodes returns the number of validators and full nodes respectively that should be used for
|
|
// the test. If the test is running in CI, more nodes are used, when running locally a single node is used by default to
|
|
// use less resources and allow the tests to run faster.
|
|
// both the number of validators and full nodes can be overwritten in a config file.
|
|
func getValidatorsAndFullNodes(chainIdx int) (int, int) {
|
|
tc := LoadConfig()
|
|
return tc.GetChainNumValidators(chainIdx), tc.GetChainNumFullNodes(chainIdx)
|
|
}
|
|
|
|
// GetMsgTransfer returns a MsgTransfer that is constructed based on the channel version
|
|
func GetMsgTransfer(portID, channelID, version string, token sdk.Coin, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string) *transfertypes.MsgTransfer {
|
|
var msg *transfertypes.MsgTransfer
|
|
switch version {
|
|
case transfertypes.V1:
|
|
msg = &transfertypes.MsgTransfer{
|
|
SourcePort: portID,
|
|
SourceChannel: channelID,
|
|
Token: token,
|
|
Sender: sender,
|
|
Receiver: receiver,
|
|
TimeoutHeight: timeoutHeight,
|
|
TimeoutTimestamp: timeoutTimestamp,
|
|
Memo: memo,
|
|
}
|
|
default:
|
|
panic(fmt.Errorf("unsupported transfer version: %s", version))
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
// SuiteName returns the name of the test suite.
|
|
func (s *E2ETestSuite) SuiteName() string {
|
|
return s.testSuiteName
|
|
}
|
|
|
|
// ThreeChainSetup provides the default behaviour to wire up 3 chains in the tests.
|
|
func ThreeChainSetup() ChainOptionConfiguration {
|
|
// copy all values of existing chains and tweak to make unique to new chain.
|
|
return func(options *ChainOptions) {
|
|
chainCSpec := *options.ChainSpecs[0] // nolint
|
|
|
|
chainCSpec.ChainID = "chainC-1"
|
|
chainCSpec.Name = "simapp-c"
|
|
|
|
options.ChainSpecs = append(options.ChainSpecs, &chainCSpec)
|
|
}
|
|
}
|
|
|
|
// DefaultChannelOpts returns the default chain options for the test suite based on the provided chains.
|
|
func DefaultChannelOpts(chains []ibc.Chain) ibc.CreateChannelOptions {
|
|
channelOptions := ibc.DefaultChannelOpts()
|
|
channelOptions.Version = transfertypes.V1
|
|
return channelOptions
|
|
}
|