diff --git a/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go b/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go index aaf50bcb930..51939cc728d 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/msg_server.go @@ -4,8 +4,12 @@ import ( "context" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v5/modules/core/24-host" ) var _ types.MsgServer = Keeper{} @@ -26,5 +30,27 @@ func (k Keeper) RegisterAccount(goCtx context.Context, msg *types.MsgRegisterAcc // SubmitTx defines a rpc handler for MsgSubmitTx func (k Keeper) SubmitTx(goCtx context.Context, msg *types.MsgSubmitTx) (*types.MsgSubmitTxResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + portID, err := icatypes.NewControllerPortID(msg.Owner) + if err != nil { + return nil, err + } + + channelID, found := k.GetActiveChannelID(ctx, msg.ConnectionId, portID) + if !found { + return nil, sdkerrors.Wrapf(icatypes.ErrActiveChannelNotFound, "failed to retrieve active channel for port %s", portID) + } + + chanCap, found := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(portID, channelID)) + if !found { + return nil, sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") + } + + _, err = k.SendTx(ctx, chanCap, msg.ConnectionId, portID, msg.PacketData, msg.TimeoutTimestamp) + if err != nil { + return nil, err + } + return &types.MsgSubmitTxResponse{}, nil } diff --git a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go index 461d16dfb44..322037a243f 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go @@ -1,9 +1,16 @@ package keeper_test import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" sdktypes "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/controller/types" + "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/controller/types" + icacontrollertypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/controller/types" + icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/types" + clienttypes "github.com/cosmos/ibc-go/v5/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types" host "github.com/cosmos/ibc-go/v5/modules/core/24-host" ibctesting "github.com/cosmos/ibc-go/v5/testing" @@ -11,7 +18,7 @@ import ( func (suite *KeeperTestSuite) TestRegisterAccount() { var ( - msg *icatypes.MsgRegisterAccount + msg *icacontrollertypes.MsgRegisterAccount expectedChannelID = "channel-0" ) @@ -63,7 +70,7 @@ func (suite *KeeperTestSuite) TestRegisterAccount() { path := NewICAPath(suite.chainA, suite.chainB) suite.coordinator.SetupConnections(path) - msg = icatypes.NewMsgRegisterAccount(ibctesting.FirstConnectionID, ibctesting.TestAccAddress, "") + msg = icacontrollertypes.NewMsgRegisterAccount(ibctesting.FirstConnectionID, ibctesting.TestAccAddress, "") tc.malleate() @@ -85,3 +92,118 @@ func (suite *KeeperTestSuite) TestRegisterAccount() { } } } + +func (suite *KeeperTestSuite) TestSubmitTx() { + var ( + path *ibctesting.Path + owner string + connectionId string + icaMsg sdk.Msg + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", func() { + owner = TestOwnerAddress + connectionId = path.EndpointA.ConnectionID + }, + true, + }, + /* + { + "failure - owner address is empty", func() { + owner = "" + connectionId = path.EndpointA.ConnectionID + }, + }, + { + "failure - active channel does not exist for connection ID", func() { + owner = TestOwnerAddress + connectionId = "connection-100" + }, + }, + { + "failure - active channel does not exist for port ID", func() { + owner = "cosmos153lf4zntqt33a4v0sm5cytrxyqn78q7kz8j8x5" + connectionId = path.EndpointA.ConnectionID + }, + }, + { + "failure - module does not own channel capability", func() { + owner = TestOwnerAddress + connectionId = path.EndpointA.ConnectionID + icaMsg = &banktypes.MsgSend{ + FromAddress: "source-address", + ToAddress: "destination-address", + Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))), + } + }, + }, + */ + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + + path = NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + tc.malleate() // malleate mutates test data + + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + portID, err := icatypes.NewControllerPortID(TestOwnerAddress) + suite.Require().NoError(err) + + // Get the address of the interchain account stored in state during handshake step + interchainAccountAddr, found := suite.chainA.GetSimApp().ICAControllerKeeper.GetInterchainAccountAddress(suite.chainA.GetContext(), path.EndpointA.ConnectionID, portID) + suite.Require().True(found) + + icaAddr, err := sdk.AccAddressFromBech32(interchainAccountAddr) + suite.Require().NoError(err) + + // Check if account is created + interchainAccount := suite.chainB.GetSimApp().AccountKeeper.GetAccount(suite.chainB.GetContext(), icaAddr) + suite.Require().Equal(interchainAccount.GetAddress().String(), interchainAccountAddr) + + // Create bank transfer message to execute on the host + icaMsg = &banktypes.MsgSend{ + FromAddress: interchainAccountAddr, + ToAddress: suite.chainB.SenderAccount.GetAddress().String(), + Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))), + } + + data, err := icatypes.SerializeCosmosTx(suite.chainA.Codec, []sdk.Msg{icaMsg}) + suite.Require().NoError(err) + + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: data, + Memo: "memo", + } + + // timeoutTimestamp set to max value with the unsigned bit shifted to sastisfy hermes timestamp conversion + // it is the responsibility of the auth module developer to ensure an appropriate timeout timestamp + timeoutTimestamp := suite.chainA.GetContext().BlockTime().Add(time.Minute).UnixNano() + + msg := types.NewMsgSubmitTx(owner, connectionId, clienttypes.NewHeight(0, 0), uint64(timeoutTimestamp), packetData) + res, err := suite.chainA.GetSimApp().ICAControllerKeeper.SubmitTx(sdk.WrapSDKContext(suite.chainA.GetContext()), msg) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().NotNil(res) + } else { + suite.Require().Error(err) + suite.Require().Nil(res) + } + }) + } +}