diff --git a/.github/workflows/e2e-manual-simd.yaml b/.github/workflows/e2e-manual-simd.yaml new file mode 100644 index 00000000000..611ad78e9c6 --- /dev/null +++ b/.github/workflows/e2e-manual-simd.yaml @@ -0,0 +1,85 @@ +name: Manual E2E (Simd) +on: + # when https://github.com/community/community/discussions/11795 is resolved + # we will be able to dynamically build up the list of valid inputs. + # for now this needs to be manual. + workflow_dispatch: + inputs: + test-entry-point: + description: 'Test entry point' + required: true + type: choice + options: + - TestTransferTestSuite + - TestIncentivizedTransferTestSuite + - TestConnectionTestSuite + - TestInterchainAccountsTestSuite + - TestInterchainAccountsGroupsTestSuite + - TestInterchainAccountsGovTestSuite + - TestIncentivizedInterchainAccountsTestSuite + - TestAuthzTransferTestSuite + chain-image: + description: 'The image to use for chain A' + required: true + type: string + default: "ghcr.io/cosmos/ibc-go-simd" + chain-binary: + description: 'Specify the chain binary to be used' + required: true + type: string + default: "simd" + chain-a-tag: + description: 'The tag to use for chain A' + required: true + type: choice + default: main + options: + - main + - v6.1.0 + - v5.2.0 + - v4.2.0 + - v4.1.1 + - v3.4.0 + - v3.3.1 + - v2.5.0 + - v2.4.2 + chain-a-tag-override: + description: 'Specify an arbitrary tag for chain A' + required: false + type: string + chain-b-tag: + default: v6.0.0 + description: 'The tag to use for chain B' + required: true + type: choice + options: + - main + - v6.1.0 + - v5.2.0 + - v4.2.0 + - v4.1.1 + - v3.4.0 + - v3.3.1 + - v2.5.0 + - v2.4.2 + chain-b-tag-override: + description: 'Specify an arbitrary tag for chain B' + required: false + type: string + relayer-tag: + description: 'The tag to use for the relayer' + required: true + default: "v2.1.2" + type: string + + +jobs: + e2e-manual: + uses: ./.github/workflows/e2e-test-workflow-call.yml + with: + chain-image: "${{ github.event.inputs.chain-image }}" + chain-a-tag: "${{ github.event.inputs.chain-a-tag-override || github.event.inputs.chain-a-tag }}" + chain-b-tag: "${{ github.event.inputs.chain-b-tag-override || github.event.inputs.chain-b-tag }}" + relayer-tag: "${{ github.event.inputs.relayer-tag }}" + test-entry-point: "${{ github.event.inputs.test-entry-point }}" + chain-binary: "${{ github.event.inputs.chain-binary }}" diff --git a/docs/ibc/proto-docs.md b/docs/ibc/proto-docs.md index 91154efa20b..5ec2d594cf0 100644 --- a/docs/ibc/proto-docs.md +++ b/docs/ibc/proto-docs.md @@ -154,6 +154,10 @@ - [Msg](#ibc.applications.transfer.v1.Msg) +- [ibc/applications/transfer/v2/authz.proto](#ibc/applications/transfer/v2/authz.proto) + - [PortChannelAmount](#ibc.applications.transfer.v2.PortChannelAmount) + - [TransferAuthorization](#ibc.applications.transfer.v2.TransferAuthorization) + - [ibc/applications/transfer/v2/packet.proto](#ibc/applications/transfer/v2/packet.proto) - [FungibleTokenPacketData](#ibc.applications.transfer.v2.FungibleTokenPacketData) @@ -2296,6 +2300,56 @@ Msg defines the ibc/transfer Msg service. + +
+ +## ibc/applications/transfer/v2/authz.proto + + + + + +### PortChannelAmount + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `source_port` | [string](#string) | | the port on which the packet will be sent | +| `source_channel` | [string](#string) | | the channel by which the packet will be sent | +| `spend_limit` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | spend limitation on the channel | +| `allowed_addresses` | [string](#string) | repeated | | + + + + + + + + +### TransferAuthorization +TransferAuthorization allows the grantee to spend up to spend_limit coins from +the granter's account for ibc transfer on a specific channel + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `allocations` | [PortChannelAmount](#ibc.applications.transfer.v2.PortChannelAmount) | repeated | port and channel amounts | + + + + + + + + + + + + + + + diff --git a/e2e/testconfig/testconfig.go b/e2e/testconfig/testconfig.go new file mode 100644 index 00000000000..fcd7e262caf --- /dev/null +++ b/e2e/testconfig/testconfig.go @@ -0,0 +1,295 @@ +package testconfig + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/strangelove-ventures/interchaintest/v7/ibc" + + "github.com/cosmos/ibc-go/e2e/semverutil" + "github.com/cosmos/ibc-go/e2e/testvalues" +) + +const ( + // ChainImageEnv specifies the image that the chains will use. If left unspecified, it will + // default to being determined based on the specified binary. E.g. ghcr.io/cosmos/ibc-go-simd + ChainImageEnv = "CHAIN_IMAGE" + // ChainATagEnv specifies the tag that Chain A will use. + ChainATagEnv = "CHAIN_A_TAG" + // ChainBTagEnv specifies the tag that Chain B will use. If unspecified + // the value will default to the same value as Chain A. + ChainBTagEnv = "CHAIN_B_TAG" + // GoRelayerTagEnv specifies the go relayer version. Defaults to "main" + GoRelayerTagEnv = "RLY_TAG" + // ChainBinaryEnv binary is the binary that will be used for both chains. + ChainBinaryEnv = "CHAIN_BINARY" + // ChainUpgradeTagEnv specifies the upgrade version tag + ChainUpgradeTagEnv = "CHAIN_UPGRADE_TAG" + // defaultBinary is the default binary that will be used by the chains. + defaultBinary = "simd" + // defaultRlyTag is the tag that will be used if no relayer tag is specified. + // all images are here https://github.com/cosmos/relayer/pkgs/container/relayer/versions + defaultRlyTag = "v2.2.0-rc2" + // defaultChainTag is the tag that will be used for the chains if none is specified. + defaultChainTag = "main" +) + +func getChainImage(binary string) string { + if binary == "" { + binary = defaultBinary + } + return fmt.Sprintf("ghcr.io/cosmos/ibc-go-%s", binary) +} + +// TestConfig holds various fields used in the E2E tests. +type TestConfig struct { + ChainAConfig ChainConfig + ChainBConfig ChainConfig + RlyTag string + UpgradeTag string +} + +type ChainConfig struct { + Image string + Tag string + Binary string +} + +// FromEnv returns a TestConfig constructed from environment variables. +func FromEnv() TestConfig { + chainBinary, ok := os.LookupEnv(ChainBinaryEnv) + if !ok { + chainBinary = defaultBinary + } + + chainATag, ok := os.LookupEnv(ChainATagEnv) + if !ok { + chainATag = defaultChainTag + } + + chainBTag, ok := os.LookupEnv(ChainBTagEnv) + if !ok { + chainBTag = chainATag + } + + rlyTag, ok := os.LookupEnv(GoRelayerTagEnv) + if !ok { + rlyTag = defaultRlyTag + } + + // TODO: remove hard coded value + rlyTag = "andrew-tendermint_v0.37" + + chainAImage := getChainImage(chainBinary) + specifiedChainImage, ok := os.LookupEnv(ChainImageEnv) + if ok { + chainAImage = specifiedChainImage + } + chainBImage := chainAImage + + upgradeTag, ok := os.LookupEnv(ChainUpgradeTagEnv) + if !ok { + upgradeTag = "" + } + + return TestConfig{ + ChainAConfig: ChainConfig{ + Image: chainAImage, + Tag: chainATag, + Binary: chainBinary, + }, + ChainBConfig: ChainConfig{ + Image: chainBImage, + Tag: chainBTag, + Binary: chainBinary, + }, + RlyTag: rlyTag, + UpgradeTag: upgradeTag, + } +} + +func GetChainATag() string { + chainATag, ok := os.LookupEnv(ChainATagEnv) + if !ok { + panic(fmt.Sprintf("no environment variable specified for %s", ChainATagEnv)) + } + return chainATag +} + +func GetChainBTag() string { + chainBTag, ok := os.LookupEnv(ChainBTagEnv) + if !ok { + return GetChainATag() + } + return chainBTag +} + +// IsCI returns true if the tests are running in CI, false is returned +// if the tests are running locally. +// Note: github actions passes a CI env value of true by default to all runners. +func IsCI() bool { + return strings.ToLower(os.Getenv("CI")) == "true" +} + +// ChainOptions stores chain configurations for the chains that will be +// created for the tests. They can be modified by passing ChainOptionConfiguration +// to E2ETestSuite.GetChains. +type ChainOptions struct { + ChainAConfig *ibc.ChainConfig + ChainBConfig *ibc.ChainConfig +} + +// ChainOptionConfiguration enables arbitrary configuration of ChainOptions. +type ChainOptionConfiguration func(options *ChainOptions) + +// DefaultChainOptions returns the default configuration for the chains. +// These options can be configured by passing configuration functions to E2ETestSuite.GetChains. +func DefaultChainOptions() ChainOptions { + tc := FromEnv() + chainACfg := newDefaultSimappConfig(tc.ChainAConfig, "simapp-a", "chain-a", "atoma") + chainBCfg := newDefaultSimappConfig(tc.ChainBConfig, "simapp-b", "chain-b", "atomb") + return ChainOptions{ + ChainAConfig: &chainACfg, + ChainBConfig: &chainBCfg, + } +} + +// newDefaultSimappConfig creates an ibc configuration for simd. +func newDefaultSimappConfig(cc ChainConfig, name, chainID, denom string) ibc.ChainConfig { + return ibc.ChainConfig{ + Type: "cosmos", + Name: name, + ChainID: chainID, + Images: []ibc.DockerImage{ + { + Repository: cc.Image, + Version: cc.Tag, + }, + }, + Bin: cc.Binary, + Bech32Prefix: "cosmos", + CoinType: fmt.Sprint(sdk.GetConfig().GetCoinType()), + Denom: denom, + GasPrices: fmt.Sprintf("0.00%s", denom), + GasAdjustment: 1.3, + TrustingPeriod: "508h", + NoHostMount: false, + ModifyGenesis: defaultModifyGenesis(), + } +} + +// govGenesisFeatureReleases represents the releases the governance module genesis +// was upgraded from v1beta1 to v1. +var govGenesisFeatureReleases = semverutil.FeatureReleases{ + MajorVersion: "v7", +} + +// defaultModifyGenesis will only modify governance params to ensure the voting period and minimum deposit +// are functional for e2e testing purposes. +// Note: this function intentionally does not use the type defined here https://github.com/tendermint/tendermint/blob/v0.37.0-rc2/types/genesis.go#L38-L46 +// and uses a map[string]interface{} instead. +// This approach prevents the field block.TimeIotaMs from being lost which happened when using the GenesisDoc type from tendermint version v0.37.0. +// ibctest performs the following steps when creating the genesis.json file for chains. +// - 1. Let the chain binary create its own genesis file. +// - 2. Apply any provided functions to modify the bytes of the file. +// - 3. Overwrite the file with the new contents. +// This is a problem because when the tendermint types change, marshalling into the type will cause us to lose +// values if the types have changed in between the version of the chain in the test and the version of tendermint +// imported by the e2e tests. +// By using a raw map[string]interface{} we preserve the values unknown to the e2e tests and can still change +// the values we care about. +// TODO: handle these genesis modifications in a way which is type safe and does not require us to rely on +// map[string]interface{} +func defaultModifyGenesis() func(ibc.ChainConfig, []byte) ([]byte, error) { + const appStateKey = "app_state" + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + genesisDocMap := map[string]interface{}{} + err := json.Unmarshal(genbz, &genesisDocMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into genesis doc: %w", err) + } + + appStateMap, ok := genesisDocMap[appStateKey].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to extract to app_state") + } + + govModuleBytes, err := json.Marshal(appStateMap[govtypes.ModuleName]) + if err != nil { + return nil, fmt.Errorf("failed to extract gov genesis bytes: %s", err) + } + + govModuleGenesisBytes, err := modifyGovAppState(chainConfig, govModuleBytes) + if err != nil { + return nil, err + } + + govModuleGenesisMap := map[string]interface{}{} + err = json.Unmarshal(govModuleGenesisBytes, &govModuleGenesisMap) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal gov genesis bytes into map: %w", err) + } + + appStateMap[govtypes.ModuleName] = govModuleGenesisMap + genesisDocMap[appStateKey] = appStateMap + + finalGenesisDocBytes, err := json.MarshalIndent(genesisDocMap, "", " ") + if err != nil { + return nil, err + } + + return finalGenesisDocBytes, nil + } +} + +// modifyGovAppState takes the existing gov app state and marshals it to either a govv1 GenesisState +// or a govv1beta1 GenesisState depending on the simapp version. +func modifyGovAppState(chainConfig ibc.ChainConfig, govAppState []byte) ([]byte, error) { + cfg := testutil.MakeTestEncodingConfig() + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + govv1.RegisterInterfaces(cfg.InterfaceRegistry) + govv1beta1.RegisterInterfaces(cfg.InterfaceRegistry) + + shouldUseGovV1 := govGenesisFeatureReleases.IsSupported(chainConfig.Images[0].Version) + + var govGenesisState gogoproto.Message + if shouldUseGovV1 { + govGenesisState = &govv1.GenesisState{} + } else { + govGenesisState = &govv1beta1.GenesisState{} + } + + if err := cdc.UnmarshalJSON(govAppState, govGenesisState); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis bytes into gov genesis state: %w", err) + } + + switch v := govGenesisState.(type) { + case *govv1.GenesisState: + if v.Params == nil { + v.Params = &govv1.Params{} + } + // set correct minimum deposit using configured denom + v.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(chainConfig.Denom, govv1beta1.DefaultMinDepositTokens)) + vp := testvalues.VotingPeriod + v.Params.VotingPeriod = &vp + case *govv1beta1.GenesisState: + v.DepositParams.MinDeposit = sdk.NewCoins(sdk.NewCoin(chainConfig.Denom, govv1beta1.DefaultMinDepositTokens)) + v.VotingParams.VotingPeriod = testvalues.VotingPeriod + } + govGenBz, err := cdc.MarshalJSON(govGenesisState) + if err != nil { + return nil, fmt.Errorf("failed to marshal gov genesis state: %w", err) + } + + return govGenBz, nil +} diff --git a/e2e/tests/transfer/authz_test.go b/e2e/tests/transfer/authz_test.go new file mode 100644 index 00000000000..c70dfe0e834 --- /dev/null +++ b/e2e/tests/transfer/authz_test.go @@ -0,0 +1,329 @@ +package transfer + +import ( + "context" + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" + test "github.com/strangelove-ventures/interchaintest/v7/testutil" + "github.com/stretchr/testify/suite" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" +) + +func TestAuthzTransferTestSuite(t *testing.T) { + suite.Run(t, new(AuthzTransferTestSuite)) +} + +type AuthzTransferTestSuite struct { + testsuite.E2ETestSuite +} + +func (suite *AuthzTransferTestSuite) TestAuthz_MsgTransfer_Succeeds() { + t := suite.T() + ctx := context.TODO() + + relayer, channelA := suite.SetupChainsRelayerAndChannel(ctx, transferChannelOptions()) + chainA, chainB := suite.GetChains() + + chainADenom := chainA.Config().Denom + + granterWallet := suite.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + granterAddress := granterWallet.FormattedAddress() + + granteeWallet := suite.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + granteeAddress := granteeWallet.FormattedAddress() + + receiverWallet := suite.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + receiverWalletAddress := receiverWallet.FormattedAddress() + + t.Run("start relayer", func(t *testing.T) { + suite.StartRelayer(relayer) + }) + + // createMsgGrantFn initializes a TransferAuthorization and broadcasts a MsgGrant message. + createMsgGrantFn := func(t *testing.T) { + transferAuth := transfertypes.TransferAuthorization{ + Allocations: []transfertypes.Allocation{ + { + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + SpendLimit: sdk.NewCoins(sdk.NewCoin(chainADenom, sdk.NewInt(testvalues.StartingTokenAmount))), + AllowList: []string{receiverWalletAddress}, + }, + }, + } + + authAny, err := codectypes.NewAnyWithValue(&transferAuth) + suite.Require().NoError(err) + + msgGrant := &authz.MsgGrant{ + Granter: granterAddress, + Grantee: granteeAddress, + Grant: authz.Grant{ + Authorization: authAny, + // no expiration + Expiration: nil, + }, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granterWallet, msgGrant) + suite.AssertValidTxResponse(resp) + suite.Require().NoError(err) + } + + // verifyGrantFn returns a test function which asserts chainA has a grant authorization + // with the given spend limit. + verifyGrantFn := func(expectedLimit int64) func(t *testing.T) { + return func(t *testing.T) { + grantAuths, err := suite.QueryGranterGrants(ctx, chainA, granterAddress) + + suite.Require().NoError(err) + suite.Require().Len(grantAuths, 1) + grantAuthorization := grantAuths[0] + + transferAuth := suite.extractTransferAuthorizationFromGrantAuthorization(grantAuthorization) + expectedSpendLimit := sdk.NewCoins(sdk.NewCoin(chainADenom, sdk.NewInt(expectedLimit))) + suite.Require().Equal(expectedSpendLimit, transferAuth.Allocations[0].SpendLimit) + } + } + + t.Run("broadcast MsgGrant", createMsgGrantFn) + + t.Run("broadcast MsgExec for ibc MsgTransfer", func(t *testing.T) { + transferMsg := transfertypes.MsgTransfer{ + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + Token: testvalues.DefaultTransferAmount(chainADenom), + Sender: granterAddress, + Receiver: receiverWalletAddress, + TimeoutHeight: suite.GetTimeoutHeight(ctx, chainB), + } + + transferAny, err := codectypes.NewAnyWithValue(&transferMsg) + suite.Require().NoError(err) + + msgExec := &authz.MsgExec{ + Grantee: granteeAddress, + Msgs: []*codectypes.Any{transferAny}, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granteeWallet, msgExec) + suite.AssertValidTxResponse(resp) + suite.Require().NoError(err) + }) + + t.Run("verify granter wallet amount", func(t *testing.T) { + actualBalance, err := suite.GetChainANativeBalance(ctx, granterWallet) + suite.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + suite.Require().Equal(expected, actualBalance) + }) + + suite.Require().NoError(test.WaitForBlocks(context.TODO(), 10, chainB)) + + t.Run("verify receiver wallet amount", func(t *testing.T) { + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) + actualBalance, err := chainB.GetBalance(ctx, receiverWalletAddress, chainBIBCToken.IBCDenom()) + suite.Require().NoError(err) + suite.Require().Equal(testvalues.IBCTransferAmount, actualBalance) + }) + + t.Run("granter grant spend limit reduced", verifyGrantFn(testvalues.StartingTokenAmount-testvalues.IBCTransferAmount)) + + t.Run("re-initialize MsgGrant", createMsgGrantFn) + + t.Run("granter grant was reinitialized", verifyGrantFn(testvalues.StartingTokenAmount)) + + t.Run("revoke access", func(t *testing.T) { + msgRevoke := authz.MsgRevoke{ + Granter: granterAddress, + Grantee: granteeAddress, + MsgTypeUrl: transfertypes.TransferAuthorization{}.MsgTypeURL(), + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granterWallet, &msgRevoke) + suite.AssertValidTxResponse(resp) + suite.Require().NoError(err) + }) + + t.Run("exec unauthorized MsgTransfer", func(t *testing.T) { + transferMsg := transfertypes.MsgTransfer{ + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + Token: testvalues.DefaultTransferAmount(chainADenom), + Sender: granterAddress, + Receiver: receiverWalletAddress, + TimeoutHeight: suite.GetTimeoutHeight(ctx, chainB), + } + + transferAny, err := codectypes.NewAnyWithValue(&transferMsg) + suite.Require().NoError(err) + + msgExec := &authz.MsgExec{ + Grantee: granteeAddress, + Msgs: []*codectypes.Any{transferAny}, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granteeWallet, msgExec) + suite.Require().NotEqual(0, resp.Code) + suite.Require().Contains(resp.RawLog, authz.ErrNoAuthorizationFound.Error()) + suite.Require().NoError(err) + }) +} + +func (suite *AuthzTransferTestSuite) TestAuthz_InvalidTransferAuthorizations() { + t := suite.T() + ctx := context.TODO() + + relayer, channelA := suite.SetupChainsRelayerAndChannel(ctx, transferChannelOptions()) + chainA, chainB := suite.GetChains() + + chainADenom := chainA.Config().Denom + + granterWallet := suite.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + granterAddress := granterWallet.FormattedAddress() + + granteeWallet := suite.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + granteeAddress := granteeWallet.FormattedAddress() + + receiverWallet := suite.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + receiverWalletAddress := receiverWallet.FormattedAddress() + + t.Run("start relayer", func(t *testing.T) { + suite.StartRelayer(relayer) + }) + + const spendLimit = 1000 + + t.Run("broadcast MsgGrant", func(t *testing.T) { + transferAuth := transfertypes.TransferAuthorization{ + Allocations: []transfertypes.Allocation{ + { + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + SpendLimit: sdk.NewCoins(sdk.NewCoin(chainADenom, sdk.NewInt(spendLimit))), + AllowList: []string{receiverWalletAddress}, + }, + }, + } + + authAny, err := codectypes.NewAnyWithValue(&transferAuth) + suite.Require().NoError(err) + + msgGrant := &authz.MsgGrant{ + Granter: granterAddress, + Grantee: granteeAddress, + Grant: authz.Grant{ + Authorization: authAny, + // no expiration + Expiration: nil, + }, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granterWallet, msgGrant) + suite.AssertValidTxResponse(resp) + suite.Require().NoError(err) + }) + + t.Run("exceed spend limit", func(t *testing.T) { + const invalidSpendAmount = spendLimit + 1 + + t.Run("broadcast MsgExec for ibc MsgTransfer", func(t *testing.T) { + transferMsg := transfertypes.MsgTransfer{ + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + Token: sdk.Coin{Denom: chainADenom, Amount: sdk.NewInt(invalidSpendAmount)}, + Sender: granterAddress, + Receiver: receiverWalletAddress, + TimeoutHeight: suite.GetTimeoutHeight(ctx, chainB), + } + + transferAny, err := codectypes.NewAnyWithValue(&transferMsg) + suite.Require().NoError(err) + + msgExec := &authz.MsgExec{ + Grantee: granteeAddress, + Msgs: []*codectypes.Any{transferAny}, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granteeWallet, msgExec) + suite.Require().NotEqual(0, resp.Code) + suite.Require().Contains(resp.RawLog, sdkerrors.ErrInsufficientFunds.Error()) + suite.Require().NoError(err) + }) + + t.Run("verify granter wallet amount", func(t *testing.T) { + actualBalance, err := suite.GetChainANativeBalance(ctx, granterWallet) + suite.Require().NoError(err) + suite.Require().Equal(testvalues.StartingTokenAmount, actualBalance) + }) + + t.Run("verify receiver wallet amount", func(t *testing.T) { + chainBIBCToken := testsuite.GetIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) + actualBalance, err := chainB.GetBalance(ctx, receiverWalletAddress, chainBIBCToken.IBCDenom()) + suite.Require().NoError(err) + suite.Require().Equal(int64(0), actualBalance) + }) + + t.Run("granter grant spend limit unchanged", func(t *testing.T) { + grantAuths, err := suite.QueryGranterGrants(ctx, chainA, granterAddress) + + suite.Require().NoError(err) + suite.Require().Len(grantAuths, 1) + grantAuthorization := grantAuths[0] + + transferAuth := suite.extractTransferAuthorizationFromGrantAuthorization(grantAuthorization) + expectedSpendLimit := sdk.NewCoins(sdk.NewCoin(chainADenom, sdk.NewInt(spendLimit))) + suite.Require().Equal(expectedSpendLimit, transferAuth.Allocations[0].SpendLimit) + }) + }) + + t.Run("send funds to invalid address", func(t *testing.T) { + invalidWallet := suite.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + invalidWalletAddress := invalidWallet.FormattedAddress() + + t.Run("broadcast MsgExec for ibc MsgTransfer", func(t *testing.T) { + transferMsg := transfertypes.MsgTransfer{ + SourcePort: channelA.PortID, + SourceChannel: channelA.ChannelID, + Token: sdk.Coin{Denom: chainADenom, Amount: sdk.NewInt(spendLimit)}, + Sender: granterAddress, + Receiver: invalidWalletAddress, + TimeoutHeight: suite.GetTimeoutHeight(ctx, chainB), + } + + transferAny, err := codectypes.NewAnyWithValue(&transferMsg) + suite.Require().NoError(err) + + msgExec := &authz.MsgExec{ + Grantee: granteeAddress, + Msgs: []*codectypes.Any{transferAny}, + } + + resp, err := suite.BroadcastMessages(context.TODO(), chainA, granteeWallet, msgExec) + suite.Require().NotEqual(0, resp.Code) + suite.Require().Contains(resp.RawLog, sdkerrors.ErrInvalidAddress.Error()) + suite.Require().NoError(err) + }) + }) +} + +// extractTransferAuthorizationFromGrantAuthorization extracts a TransferAuthorization from the given +// GrantAuthorization. +func (suite *AuthzTransferTestSuite) extractTransferAuthorizationFromGrantAuthorization(grantAuth *authz.GrantAuthorization) *transfertypes.TransferAuthorization { + cfg := testsuite.EncodingConfig() + var authorization authz.Authorization + err := cfg.InterfaceRegistry.UnpackAny(grantAuth.Authorization, &authorization) + suite.Require().NoError(err) + + transferAuth, ok := authorization.(*transfertypes.TransferAuthorization) + suite.Require().True(ok) + return transferAuth +} diff --git a/e2e/testsuite/codec.go b/e2e/testsuite/codec.go new file mode 100644 index 00000000000..db0fa340b6c --- /dev/null +++ b/e2e/testsuite/codec.go @@ -0,0 +1,48 @@ +package testsuite + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdkcodec "github.com/cosmos/cosmos-sdk/crypto/codec" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + grouptypes "github.com/cosmos/cosmos-sdk/x/group" + proposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + + icacontrollertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" + feetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + simappparams "github.com/cosmos/ibc-go/v7/testing/simapp/params" +) + +func Codec() *codec.ProtoCodec { + cdc, _ := codecAndEncodingConfig() + return cdc +} + +func EncodingConfig() simappparams.EncodingConfig { + _, cfg := codecAndEncodingConfig() + return cfg +} + +func codecAndEncodingConfig() (*codec.ProtoCodec, simappparams.EncodingConfig) { + cfg := simappparams.MakeTestEncodingConfig() + banktypes.RegisterInterfaces(cfg.InterfaceRegistry) + govv1beta1.RegisterInterfaces(cfg.InterfaceRegistry) + govv1.RegisterInterfaces(cfg.InterfaceRegistry) + authtypes.RegisterInterfaces(cfg.InterfaceRegistry) + feetypes.RegisterInterfaces(cfg.InterfaceRegistry) + icacontrollertypes.RegisterInterfaces(cfg.InterfaceRegistry) + sdkcodec.RegisterInterfaces(cfg.InterfaceRegistry) + grouptypes.RegisterInterfaces(cfg.InterfaceRegistry) + proposaltypes.RegisterInterfaces(cfg.InterfaceRegistry) + authz.RegisterInterfaces(cfg.InterfaceRegistry) + transfertypes.RegisterInterfaces(cfg.InterfaceRegistry) + clienttypes.RegisterInterfaces(cfg.InterfaceRegistry) + + cdc := codec.NewProtoCodec(cfg.InterfaceRegistry) + return cdc, cfg +} diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go new file mode 100644 index 00000000000..05fb56232ba --- /dev/null +++ b/e2e/testsuite/testsuite.go @@ -0,0 +1,575 @@ +package testsuite + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/authz" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + grouptypes "github.com/cosmos/cosmos-sdk/x/group" + paramsproposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + intertxtypes "github.com/cosmos/interchain-accounts/x/inter-tx/types" + dockerclient "github.com/docker/docker/client" + interchaintest "github.com/strangelove-ventures/interchaintest/v7" + "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v7/ibc" + "github.com/strangelove-ventures/interchaintest/v7/testreporter" + test "github.com/strangelove-ventures/interchaintest/v7/testutil" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/cosmos/ibc-go/e2e/testconfig" + "github.com/cosmos/ibc-go/e2e/testvalues" + controllertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" + feetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/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 = 500000 + // emptyLogs is the string value returned from `BroadcastMessages`. There are some situations in which + // the result is empty, when this happens we include the raw logs instead to get as much information + // amount the failure as possible. + emptyLogs = "[]" +) + +// E2ETestSuite has methods and functionality which can be shared among all test suites. +type E2ETestSuite struct { + suite.Suite + + grpcClients map[string]GRPCClients + paths map[string]path + logger *zap.Logger + DockerClient *dockerclient.Client + network string + startRelayerFn func(relayer ibc.Relayer) + + // pathNameIndex is the latest index to be used for generating paths + pathNameIndex uint64 +} + +// GRPCClients holds a reference to any GRPC clients that are needed by the tests. +// These should typically be used for query clients only. If we need to make changes, we should +// use E2ETestSuite.BroadcastMessages to broadcast transactions instead. +type GRPCClients struct { + ClientQueryClient clienttypes.QueryClient + ChannelQueryClient channeltypes.QueryClient + FeeQueryClient feetypes.QueryClient + ICAQueryClient controllertypes.QueryClient + InterTxQueryClient intertxtypes.QueryClient + + // SDK query clients + GovQueryClient govtypesv1beta1.QueryClient + GovQueryClientV1 govtypesv1.QueryClient + GroupsQueryClient grouptypes.QueryClient + ParamsQueryClient paramsproposaltypes.QueryClient + AuthQueryClient authtypes.QueryClient + AuthZQueryClient authz.QueryClient +} + +// path is a pairing of two chains which will be used in a test. +type path struct { + chainA, chainB *cosmos.CosmosChain +} + +// newPath returns a path built from the given chains. +func newPath(chainA, chainB *cosmos.CosmosChain) path { + return path{ + chainA: chainA, + chainB: chainB, + } +} + +// 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, chainOpts ...testconfig.ChainOptionConfiguration) (ibc.Wallet, ibc.Wallet) { + chainA, chainB := s.GetChains(chainOpts...) + chainAAccountBytes, err := chainA.GetAddress(ctx, ChainARelayerName) + s.Require().NoError(err) + + chainBAccountBytes, err := chainB.GetAddress(ctx, ChainBRelayerName) + s.Require().NoError(err) + + chainARelayerUser := cosmos.NewWallet(ChainARelayerName, chainAAccountBytes, "", chainA.Config()) + chainBRelayerUser := cosmos.NewWallet(ChainBRelayerName, chainBAccountBytes, "", chainB.Config()) + + return chainARelayerUser, chainBRelayerUser +} + +// SetupChainsRelayerAndChannel create two chains, a relayer, establishes a connection and creates a channel +// using the given channel options. The relayer returned by this function has not yet started. It should be started +// with E2ETestSuite.StartRelayer if needed. +// This should be called at the start of every test, unless fine grained control is required. +func (s *E2ETestSuite) SetupChainsRelayerAndChannel(ctx context.Context, channelOpts ...func(*ibc.CreateChannelOptions)) (ibc.Relayer, ibc.ChannelOutput) { + chainA, chainB := s.GetChains() + + r := newCosmosRelayer(s.T(), testconfig.FromEnv(), s.logger, s.DockerClient, s.network) + + pathName := s.generatePathName() + + channelOptions := ibc.DefaultChannelOpts() + for _, opt := range channelOpts { + opt(&channelOptions) + } + + ic := interchaintest.NewInterchain(). + AddChain(chainA). + AddChain(chainB). + AddRelayer(r, "r"). + AddLink(interchaintest.InterchainLink{ + Chain1: chainA, + Chain2: chainB, + Relayer: r, + Path: pathName, + CreateChannelOpts: channelOptions, + }) + + eRep := s.GetRelayerExecReporter() + s.Require().NoError(ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ + TestName: s.T().Name(), + Client: s.DockerClient, + NetworkID: s.network, + })) + + s.startRelayerFn = func(relayer ibc.Relayer) { + err := relayer.StartRelayer(ctx, eRep, pathName) + s.Require().NoError(err, fmt.Sprintf("failed to start relayer: %s", err)) + s.T().Cleanup(func() { + if !s.T().Failed() { + if err := relayer.StopRelayer(ctx, eRep); err != nil { + s.T().Logf("error stopping relayer: %v", err) + } + } + }) + // wait for relayer to start. + time.Sleep(time.Second * 10) + } + + s.InitGRPCClients(chainA) + s.InitGRPCClients(chainB) + + chainAChannels, err := r.GetChannels(ctx, eRep, chainA.Config().ChainID) + s.Require().NoError(err) + return r, chainAChannels[len(chainAChannels)-1] +} + +// generatePathName generates the path name using the test suites name +func (s *E2ETestSuite) generatePathName() string { + pathName := fmt.Sprintf("%s-path-%d", s.T().Name(), s.pathNameIndex) + s.pathNameIndex++ + return strings.ReplaceAll(pathName, "/", "-") +} + +// generatePath generates the path name using the test suites name +func (s *E2ETestSuite) generatePath(ctx context.Context, relayer ibc.Relayer) string { + chainA, chainB := s.GetChains() + chainAID := chainA.Config().ChainID + chainBID := chainB.Config().ChainID + + pathName := s.generatePathName() + err := relayer.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, relayer ibc.Relayer, opts ibc.CreateClientOptions) { + pathName := s.generatePath(ctx, relayer) + err := relayer.CreateClients(ctx, s.GetRelayerExecReporter(), pathName, opts) + s.Require().NoError(err) +} + +// UpdateClients updates clients on chainA and chainB +func (s *E2ETestSuite) UpdateClients(ctx context.Context, relayer ibc.Relayer, pathName string) { + err := relayer.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(chainOpts ...testconfig.ChainOptionConfiguration) (*cosmos.CosmosChain, *cosmos.CosmosChain) { + if s.paths == nil { + s.paths = map[string]path{} + } + + path, ok := s.paths[s.T().Name()] + if ok { + return path.chainA, path.chainB + } + + chainOptions := testconfig.DefaultChainOptions() + for _, opt := range chainOpts { + opt(&chainOptions) + } + + chainA, chainB := s.createCosmosChains(chainOptions) + path = newPath(chainA, chainB) + s.paths[s.T().Name()] = path + + return path.chainA, path.chainB +} + +// 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 both chain A and chain B. +func (s *E2ETestSuite) BroadcastMessages(ctx context.Context, chain *cosmos.CosmosChain, user ibc.Wallet, msgs ...sdk.Msg) (sdk.TxResponse, error) { + broadcaster := cosmos.NewBroadcaster(s.T(), chain) + + 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) + }) + + resp, err := cosmos.BroadcastTx(ctx, broadcaster, user, msgs...) + if err != nil { + return sdk.TxResponse{}, err + } + + chainA, chainB := s.GetChains() + err = test.WaitForBlocks(ctx, 2, chainA, chainB) + return resp, err +} + +// RegisterCounterPartyPayee broadcasts a MsgRegisterCounterpartyPayee message. +func (s *E2ETestSuite) RegisterCounterPartyPayee(ctx context.Context, chain *cosmos.CosmosChain, + user ibc.Wallet, portID, channelID, relayerAddr, counterpartyPayeeAddr string, +) (sdk.TxResponse, error) { + msg := feetypes.NewMsgRegisterCounterpartyPayee(portID, channelID, relayerAddr, counterpartyPayeeAddr) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// PayPacketFeeAsync broadcasts a MsgPayPacketFeeAsync message. +func (s *E2ETestSuite) PayPacketFeeAsync( + ctx context.Context, + chain *cosmos.CosmosChain, + user ibc.Wallet, + packetID channeltypes.PacketId, + packetFee feetypes.PacketFee, +) (sdk.TxResponse, error) { + msg := feetypes.NewMsgPayPacketFeeAsync(packetID, packetFee) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// GetRelayerWallets returns the relayer wallets associated with the chains. +func (s *E2ETestSuite) GetRelayerWallets(relayer ibc.Relayer) (ibc.Wallet, ibc.Wallet, error) { + chainA, chainB := s.GetChains() + chainARelayerWallet, ok := relayer.GetWallet(chainA.Config().ChainID) + if !ok { + return nil, nil, fmt.Errorf("unable to find chain A relayer wallet") + } + + chainBRelayerWallet, ok := relayer.GetWallet(chainB.Config().ChainID) + if !ok { + return nil, nil, fmt.Errorf("unable to find chain B relayer wallet") + } + return chainARelayerWallet, chainBRelayerWallet, nil +} + +// RecoverRelayerWallets adds the corresponding relayer 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, relayer ibc.Relayer) error { + chainARelayerWallet, chainBRelayerWallet, err := s.GetRelayerWallets(relayer) + if err != nil { + return err + } + + chainA, chainB := s.GetChains() + + if err := chainA.RecoverKey(ctx, ChainARelayerName, chainARelayerWallet.Mnemonic()); err != nil { + return fmt.Errorf("could not recover relayer wallet on chain A: %s", err) + } + if err := chainB.RecoverKey(ctx, ChainBRelayerName, chainBRelayerWallet.Mnemonic()); err != nil { + return fmt.Errorf("could not recover relayer wallet on chain B: %s", err) + } + return nil +} + +// Transfer broadcasts a MsgTransfer message. +func (s *E2ETestSuite) Transfer(ctx context.Context, chain *cosmos.CosmosChain, user ibc.Wallet, + portID, channelID string, token sdk.Coin, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, memo string, +) (sdk.TxResponse, error) { + msg := transfertypes.NewMsgTransfer(portID, channelID, token, sender, receiver, timeoutHeight, timeoutTimestamp, memo) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// StartRelayer starts the given relayer. +func (s *E2ETestSuite) StartRelayer(relayer ibc.Relayer) { + if s.startRelayerFn == nil { + panic("cannot start relayer before it is created!") + } + + s.startRelayerFn(relayer) +} + +// StopRelayer stops the given relayer. +func (s *E2ETestSuite) StopRelayer(ctx context.Context, relayer ibc.Relayer) { + err := relayer.StopRelayer(ctx, s.GetRelayerExecReporter()) + s.Require().NoError(err) +} + +// CreateUserOnChainA creates a user with the given amount of funds on chain A. +func (s *E2ETestSuite) CreateUserOnChainA(ctx context.Context, amount int64) ibc.Wallet { + chainA, _ := s.GetChains() + return interchaintest.GetAndFundTestUsers(s.T(), ctx, strings.ReplaceAll(s.T().Name(), " ", "-"), amount, chainA)[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 { + _, chainB := s.GetChains() + return interchaintest.GetAndFundTestUsers(s.T(), ctx, strings.ReplaceAll(s.T().Name(), " ", "-"), amount, chainB)[0] +} + +// 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.GetChains() + return GetNativeChainBalance(ctx, chainA, 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.GetChains() + return GetNativeChainBalance(ctx, chainB, user) +} + +// GetChainGRCPClients gets the GRPC clients associated with the given chain. +func (s *E2ETestSuite) GetChainGRCPClients(chain ibc.Chain) GRPCClients { + cs, ok := s.grpcClients[chain.Config().ChainID] + s.Require().True(ok, "chain %s does not have GRPC clients", chain.Config().ChainID) + return cs +} + +// InitGRPCClients establishes GRPC clients with the given chain. +// The created GRPCClients can be retrieved with GetChainGRCPClients. +func (s *E2ETestSuite) InitGRPCClients(chain *cosmos.CosmosChain) { + // Create a connection to the gRPC server. + grpcConn, err := grpc.Dial( + chain.GetHostGRPCAddress(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + s.Require().NoError(err) + s.T().Cleanup(func() { + if err := grpcConn.Close(); err != nil { + s.T().Logf("failed closing GRPC connection to chain %s: %s", chain.Config().ChainID, err) + } + }) + + if s.grpcClients == nil { + s.grpcClients = make(map[string]GRPCClients) + } + + s.grpcClients[chain.Config().ChainID] = GRPCClients{ + ClientQueryClient: clienttypes.NewQueryClient(grpcConn), + ChannelQueryClient: channeltypes.NewQueryClient(grpcConn), + FeeQueryClient: feetypes.NewQueryClient(grpcConn), + ICAQueryClient: controllertypes.NewQueryClient(grpcConn), + InterTxQueryClient: intertxtypes.NewQueryClient(grpcConn), + GovQueryClient: govtypesv1beta1.NewQueryClient(grpcConn), + GovQueryClientV1: govtypesv1.NewQueryClient(grpcConn), + GroupsQueryClient: grouptypes.NewQueryClient(grpcConn), + ParamsQueryClient: paramsproposaltypes.NewQueryClient(grpcConn), + AuthQueryClient: authtypes.NewQueryClient(grpcConn), + AuthZQueryClient: authz.NewQueryClient(grpcConn), + } +} + +// AssertValidTxResponse verifies that an sdk.TxResponse +// has non-empty values. +func (s *E2ETestSuite) AssertValidTxResponse(resp sdk.TxResponse) { + respLogsMsg := resp.Logs.String() + if respLogsMsg == emptyLogs { + respLogsMsg = resp.RawLog + } + s.Require().NotEqual(int64(0), resp.GasUsed, respLogsMsg) + s.Require().NotEqual(int64(0), resp.GasWanted, respLogsMsg) + s.Require().NotEmpty(resp.Events, respLogsMsg) + s.Require().NotEmpty(resp.Data, respLogsMsg) +} + +// 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 *cosmos.CosmosChain, portID, channelID string, sequence uint64) { + commitment, _ := s.QueryPacketCommitment(ctx, chain, portID, channelID, sequence) + s.Require().Empty(commitment) +} + +// createCosmosChains creates two separate chains in docker containers. +// test and can be retrieved with GetChains. +func (s *E2ETestSuite) createCosmosChains(chainOptions testconfig.ChainOptions) (*cosmos.CosmosChain, *cosmos.CosmosChain) { + client, network := interchaintest.DockerSetup(s.T()) + + s.logger = zap.NewExample() + s.DockerClient = client + s.network = network + + logger := zaptest.NewLogger(s.T()) + + numValidators, numFullNodes := getValidatorsAndFullNodes() + + chainA := cosmos.NewCosmosChain(s.T().Name(), *chainOptions.ChainAConfig, numValidators, numFullNodes, logger) + chainB := cosmos.NewCosmosChain(s.T().Name(), *chainOptions.ChainBConfig, numValidators, numFullNodes, logger) + return chainA, chainB +} + +// 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()) +} + +// 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 *cosmos.CosmosChain) clienttypes.Height { + height, err := chain.Height(ctx) + s.Require().NoError(err) + return clienttypes.NewHeight(clienttypes.ParseChainID(chain.Config().ChainID), uint64(height)+1000) +} + +// GetNativeChainBalance returns the balance of a specific user on a chain using the native denom. +func GetNativeChainBalance(ctx context.Context, chain ibc.Chain, user ibc.Wallet) (int64, error) { + bal, err := chain.GetBalance(ctx, user.FormattedAddress(), chain.Config().Denom) + if err != nil { + return -1, err + } + return bal, nil +} + +// ExecuteGovProposal submits the given 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) ExecuteGovProposal(ctx context.Context, chain *cosmos.CosmosChain, user ibc.Wallet, content govtypesv1beta1.Content) { + 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) + + txResp, err := s.BroadcastMessages(ctx, chain, user, msgSubmitProposal) + s.Require().NoError(err) + s.AssertValidTxResponse(txResp) + + // TODO: replace with parsed proposal ID from MsgSubmitProposalResponse + // https://github.com/cosmos/ibc-go/issues/2122 + + proposal, err := s.QueryProposal(ctx, chain, 1) + s.Require().NoError(err) + s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposal.Status) + + err = chain.VoteOnProposalAllValidators(ctx, "1", cosmos.ProposalVoteYes) + s.Require().NoError(err) + + // ensure voting period has not passed before validators finished voting + proposal, err = s.QueryProposal(ctx, chain, 1) + s.Require().NoError(err) + s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposal.Status) + + time.Sleep(testvalues.VotingPeriod) // pass proposal + + proposal, err = s.QueryProposal(ctx, chain, 1) + s.Require().NoError(err) + s.Require().Equal(govtypesv1beta1.StatusPassed, proposal.Status) +} + +// ExecuteGovProposalV1 submits a 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) ExecuteGovProposalV1(ctx context.Context, msg sdk.Msg, chain *cosmos.CosmosChain, user ibc.Wallet, proposalID uint64) { + sender, err := sdk.AccAddressFromBech32(user.FormattedAddress()) + s.Require().NoError(err) + + msgs := []sdk.Msg{msg} + msgSubmitProposal, err := govtypesv1.NewMsgSubmitProposal(msgs, sdk.NewCoins(sdk.NewCoin(chain.Config().Denom, govtypesv1.DefaultMinDepositTokens)), sender.String(), "", fmt.Sprintf("e2e gov proposal: %d", proposalID), fmt.Sprintf("executing gov proposal %d", proposalID)) + s.Require().NoError(err) + + resp, err := s.BroadcastMessages(ctx, chain, user, msgSubmitProposal) + s.AssertValidTxResponse(resp) + s.Require().NoError(err) + + s.Require().NoError(chain.VoteOnProposalAllValidators(ctx, strconv.Itoa(int(proposalID)), cosmos.ProposalVoteYes)) + + time.Sleep(testvalues.VotingPeriod) + + proposal, err := s.QueryProposalV1(ctx, chain, proposalID) + s.Require().NoError(err) + s.Require().Equal(govtypesv1.StatusPassed, proposal.Status) +} + +// QueryModuleAccountAddress returns the sdk.AccAddress of a given module name. +func (s *E2ETestSuite) QueryModuleAccountAddress(ctx context.Context, moduleName string, chain *cosmos.CosmosChain) (sdk.AccAddress, error) { + authClient := s.GetChainGRCPClients(chain).AuthQueryClient + + resp, err := authClient.ModuleAccountByName(ctx, &authtypes.QueryModuleAccountByNameRequest{ + Name: moduleName, + }) + if err != nil { + return nil, err + } + + cfg := EncodingConfig() + + var account authtypes.AccountI + if err := cfg.InterfaceRegistry.UnpackAny(resp.Account, &account); err != nil { + return nil, err + } + moduleAccount, ok := account.(authtypes.ModuleAccountI) + if !ok { + return nil, errors.New(fmt.Sprintf("failed to cast account: %T as ModuleAccount", moduleAccount)) + } + + return moduleAccount.GetAddress(), nil +} + +// QueryGranterGrants returns all GrantAuthorizations for the given granterAddress. +func (s *E2ETestSuite) QueryGranterGrants(ctx context.Context, chain *cosmos.CosmosChain, granterAddress string) ([]*authz.GrantAuthorization, error) { + authzClient := s.GetChainGRCPClients(chain).AuthZQueryClient + queryRequest := &authz.QueryGranterGrantsRequest{ + Granter: granterAddress, + } + + grants, err := authzClient.GranterGrants(ctx, queryRequest) + if err != nil { + return nil, err + } + + return grants.Grants, nil +} + +// GetIBCToken returns the denomination of the full token denom sent to the receiving channel +func GetIBCToken(fullTokenDenom string, portID, channelID string) transfertypes.DenomTrace { + return transfertypes.ParseDenomTrace(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 to +// use less resources and allow the tests to run faster. +func getValidatorsAndFullNodes() (int, int) { + if testconfig.IsCI() { + return 4, 1 + } + return 1, 0 +} diff --git a/e2e/testvalues/values.go b/e2e/testvalues/values.go new file mode 100644 index 00000000000..c4c09e68781 --- /dev/null +++ b/e2e/testvalues/values.go @@ -0,0 +1,45 @@ +package testvalues + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/strangelove-ventures/interchaintest/v7/ibc" + + feetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" +) + +const ( + StartingTokenAmount int64 = 100_000_000 + IBCTransferAmount int64 = 10_000 + InvalidAddress string = "