diff --git a/.github/workflows/e2e-manual.yaml b/.github/workflows/e2e-manual.yaml index 9aa3679fc09..519c3b5d4f4 100644 --- a/.github/workflows/e2e-manual.yaml +++ b/.github/workflows/e2e-manual.yaml @@ -10,6 +10,7 @@ on: required: true type: choice options: + - TestTransferTestSuite - TestFeeMiddlewareTestSuite chain-a-image: description: 'The image to use for chain A' diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index 979dcb617ce..16132793578 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -292,6 +292,13 @@ func (s *E2ETestSuite) AssertValidTxResponse(resp sdk.TxResponse) { 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) { diff --git a/e2e/transfer_test.go b/e2e/transfer_test.go new file mode 100644 index 00000000000..01c112cb66f --- /dev/null +++ b/e2e/transfer_test.go @@ -0,0 +1,129 @@ +package e2e + +/* +The TransferTestSuite assumes both chainA and chainB support version ics20-1. +*/ + +import ( + "context" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/strangelove-ventures/ibctest" + "github.com/strangelove-ventures/ibctest/chain/cosmos" + "github.com/strangelove-ventures/ibctest/ibc" + "github.com/strangelove-ventures/ibctest/test" + "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/v5/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" +) + +func TestTransferTestSuite(t *testing.T) { + suite.Run(t, new(TransferTestSuite)) +} + +type TransferTestSuite struct { + testsuite.E2ETestSuite +} + +// Transfer broadcasts a MsgTransfer message. +func (s *TransferTestSuite) Transfer(ctx context.Context, chain *cosmos.CosmosChain, user *ibctest.User, + portID, channelID string, token sdk.Coin, sender, receiver string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, +) (sdk.TxResponse, error) { + msg := transfertypes.NewMsgTransfer(portID, channelID, token, sender, receiver, timeoutHeight, timeoutTimestamp) + return s.BroadcastMessages(ctx, chain, user, msg) +} + +// TestMsgTransfer_Succeeds_Nonincentivized will test sending successful IBC transfers from chainA to chainB. +// The transfer will occur over a basic transfer channel (non incentivized) and both native and non-native tokens +// will be sent forwards and backwards in the IBC transfer timeline (both chains will act as source and receiver chains). +func (s *TransferTestSuite) TestMsgTransfer_Succeeds_Nonincentivized() { + t := s.T() + ctx := context.TODO() + + relayer, channelA := s.SetupChainsRelayerAndChannel(ctx, transferChannelOptions()) + chainA, chainB := s.GetChains() + + chainADenom := chainA.Config().Denom + + chainAWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + chainAAddress := chainAWallet.Bech32Address(chainA.Config().Bech32Prefix) + + chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + chainBAddress := chainBWallet.Bech32Address(chainB.Config().Bech32Prefix) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + t.Run("native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { + transferTxResp, err := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferAmount(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0) + s.Require().NoError(err) + s.AssertValidTxResponse(transferTxResp) + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("start relayer", func(t *testing.T) { + s.StartRelayer(relayer) + }) + + chainBIBCToken := s.getIBCToken(chainADenom, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID) + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainA, channelA.PortID, channelA.ChannelID, 1) + + actualBalance, err := chainB.GetBalance(ctx, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + expected := testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + }) + + t.Run("non-native IBC token transfer from chainB to chainA, receiver is source of tokens", func(t *testing.T) { + transferTxResp, err := s.Transfer(ctx, chainB, chainBWallet, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, testvalues.DefaultTransferAmount(chainBIBCToken.IBCDenom()), chainBAddress, chainAAddress, s.GetTimeoutHeight(ctx, chainA), 0) + s.Require().NoError(err) + s.AssertValidTxResponse(transferTxResp) + }) + + t.Run("tokens are escrowed", func(t *testing.T) { + actualBalance, err := chainB.GetBalance(ctx, chainBAddress, chainBIBCToken.IBCDenom()) + s.Require().NoError(err) + + s.Require().Equal(int64(0), actualBalance) + }) + + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") + + t.Run("packets are relayed", func(t *testing.T) { + s.AssertPacketRelayed(ctx, chainB, channelA.Counterparty.PortID, channelA.Counterparty.ChannelID, 1) + + actualBalance, err := s.GetChainANativeBalance(ctx, chainAWallet) + s.Require().NoError(err) + + expected := testvalues.StartingTokenAmount + s.Require().Equal(expected, actualBalance) + }) +} + +// transferChannelOptions configures both of the chains to have non-incentivized transfer channels. +func transferChannelOptions() func(options *ibc.CreateChannelOptions) { + return func(opts *ibc.CreateChannelOptions) { + opts.Version = transfertypes.Version + opts.SourcePortName = transfertypes.PortID + opts.DestPortName = transfertypes.PortID + } +} + +// getIBCToken returns the denomination of the full token denom sent to the receiving channel +func (s *TransferTestSuite) getIBCToken(fullTokenDenom string, portID, channelID string) transfertypes.DenomTrace { + return transfertypes.ParseDenomTrace(fmt.Sprintf("%s/%s/%s", portID, channelID, fullTokenDenom)) +}