diff --git a/runtime/v2/module.go b/runtime/v2/module.go index a2cca4a96337..8ef338f52c2a 100644 --- a/runtime/v2/module.go +++ b/runtime/v2/module.go @@ -15,10 +15,12 @@ import ( autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/branch" "cosmossdk.io/core/comet" "cosmossdk.io/core/event" "cosmossdk.io/core/header" "cosmossdk.io/core/registry" + "cosmossdk.io/core/router" "cosmossdk.io/core/store" "cosmossdk.io/core/transaction" "cosmossdk.io/depinject" @@ -213,27 +215,66 @@ func ProvideEnvironment( memKvService store.MemoryStoreService, headerService header.Service, eventService event.Service, + branchService branch.Service, + routerBuilder RouterServiceBuilder, ) appmodulev2.Environment { return appmodulev2.Environment{ Logger: logger, - BranchService: stf.BranchService{}, + BranchService: branchService, EventService: eventService, GasService: stf.NewGasMeterService(), HeaderService: headerService, - QueryRouterService: stf.NewQueryRouterService(), - MsgRouterService: stf.NewMsgRouterService([]byte(key.Name())), + QueryRouterService: routerBuilder.BuildQueryRouter(), + MsgRouterService: routerBuilder.BuildMsgRouter([]byte(key.Name())), TransactionService: services.NewContextAwareTransactionService(), KVStoreService: kvService, MemStoreService: memKvService, } } +// RouterServiceBuilder builds the msg router and query router service during app initialization. +// this is mainly use for testing to override message router service in the environment and not in stf. +type RouterServiceBuilder interface { + // BuildMsgRouter return a msg router service. + // - actor is the module store key. + BuildMsgRouter(actor []byte) router.Service + BuildQueryRouter() router.Service +} + +type RouterServiceFactory func([]byte) router.Service + +// routerBuilder implements RouterServiceBuilder +type routerBuilder struct { + msgRouterServiceFactory RouterServiceFactory + queryRouter router.Service +} + +func NewRouterBuilder( + msgRouterServiceFactory RouterServiceFactory, + queryRouter router.Service, +) RouterServiceBuilder { + return routerBuilder{ + msgRouterServiceFactory: msgRouterServiceFactory, + queryRouter: queryRouter, + } +} + +func (b routerBuilder) BuildMsgRouter(actor []byte) router.Service { + return b.msgRouterServiceFactory(actor) +} + +func (b routerBuilder) BuildQueryRouter() router.Service { + return b.queryRouter +} + // DefaultServiceBindings provides default services for the following service interfaces: // - store.KVStoreServiceFactory // - header.Service // - comet.Service // - event.Service // - store/v2/root.Builder +// - branch.Service +// - RouterServiceBuilder // // They are all required. For most use cases these default services bindings should be sufficient. // Power users (or tests) may wish to provide their own services bindings, in which case they must @@ -246,16 +287,23 @@ func DefaultServiceBindings() depinject.Config { stf.NewKVStoreService(actor), ) } + routerBuilder RouterServiceBuilder = routerBuilder{ + msgRouterServiceFactory: stf.NewMsgRouterService, + queryRouter: stf.NewQueryRouterService(), + } cometService comet.Service = &services.ContextAwareCometInfoService{} headerService = services.NewGenesisHeaderService(stf.HeaderService{}) eventService = services.NewGenesisEventService(stf.NewEventService()) storeBuilder = root.NewBuilder() + branchService = stf.BranchService{} ) return depinject.Supply( kvServiceFactory, + routerBuilder, headerService, cometService, eventService, storeBuilder, + branchService, ) } diff --git a/tests/integration/v2/app.go b/tests/integration/v2/app.go index aa5420d425dd..0554f9b148d8 100644 --- a/tests/integration/v2/app.go +++ b/tests/integration/v2/app.go @@ -14,6 +14,7 @@ import ( cmttypes "github.com/cometbft/cometbft/types" "github.com/stretchr/testify/require" + corebranch "cosmossdk.io/core/branch" "cosmossdk.io/core/comet" corecontext "cosmossdk.io/core/context" "cosmossdk.io/core/server" @@ -53,6 +54,8 @@ const ( type stateMachineTx = transaction.Tx +type handler = func(ctx context.Context) (transaction.Msg, error) + // DefaultConsensusParams defines the default CometBFT consensus params used in // SimApp testing. var DefaultConsensusParams = &cmtproto.ConsensusParams{ @@ -88,6 +91,11 @@ type StartupConfig struct { GenesisAccounts []GenesisAccount // HomeDir defines the home directory of the app where config and data will be stored. HomeDir string + // BranchService defines the custom branch service to be used in the app. + BranchService corebranch.Service + // RouterServiceBuilder defines the custom builder + // for msg router and query router service to be used in the app. + RouterServiceBuilder runtime.RouterServiceBuilder } func DefaultStartUpConfig(t *testing.T) StartupConfig { @@ -113,6 +121,26 @@ func DefaultStartUpConfig(t *testing.T) StartupConfig { GenesisBehavior: Genesis_COMMIT, GenesisAccounts: []GenesisAccount{ga}, HomeDir: homedir, + BranchService: stf.BranchService{}, + RouterServiceBuilder: runtime.NewRouterBuilder( + stf.NewMsgRouterService, stf.NewQueryRouterService(), + ), + } +} + +// RunMsgConfig defines the run message configuration. +type RunMsgConfig struct { + Commit bool +} + +// Option is a function that can be used to configure the integration app. +type Option func(*RunMsgConfig) + +// WithAutomaticCommit enables automatic commit. +// This means that the integration app will automatically commit the state after each msg. +func WithAutomaticCommit() Option { + return func(cfg *RunMsgConfig) { + cfg.Commit = true } } @@ -159,6 +187,8 @@ func NewApp( kvFactory, &eventService{}, storeBuilder, + startupConfig.BranchService, + startupConfig.RouterServiceBuilder, ), depinject.Invoke( std.RegisterInterfaces, @@ -397,6 +427,31 @@ func (a *App) SignCheckDeliver( return txResult } +// RunMsg runs the handler for a transaction message. +// It required the context to have the integration context. +// a new state is committed if the option WithAutomaticCommit is set in options. +func (app *App) RunMsg(t *testing.T, ctx context.Context, handler handler, option ...Option) (resp transaction.Msg, err error) { + // set options + cfg := &RunMsgConfig{} + for _, opt := range option { + opt(cfg) + } + + // need to have integration context + integrationCtx, ok := ctx.Value(contextKey).(*integrationContext) + require.True(t, ok) + + resp, err = handler(ctx) + + if cfg.Commit { + app.lastHeight++ + _, err := app.Commit(integrationCtx.state) + require.NoError(t, err) + } + + return resp, err +} + // CheckBalance checks the balance of the given address. func (a *App) CheckBalance( t *testing.T, ctx context.Context, addr sdk.AccAddress, expected sdk.Coins, keeper bankkeeper.Keeper, diff --git a/tests/integration/v2/auth/accounts_retro_compatibility_test.go b/tests/integration/v2/auth/accounts_retro_compatibility_test.go new file mode 100644 index 000000000000..0889a79446e5 --- /dev/null +++ b/tests/integration/v2/auth/accounts_retro_compatibility_test.go @@ -0,0 +1,185 @@ +package auth + +import ( + "context" + "errors" + "testing" + + gogotypes "github.com/cosmos/gogoproto/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "cosmossdk.io/x/accounts/accountstd" + basev1 "cosmossdk.io/x/accounts/defaults/base/v1" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +var _ accountstd.Interface = mockRetroCompatAccount{} + +type mockRetroCompatAccount struct { + retroCompat *authtypes.QueryLegacyAccountResponse + address []byte +} + +var ( + valid = &mockRetroCompatAccount{ + retroCompat: &authtypes.QueryLegacyAccountResponse{ + Account: &codectypes.Any{}, + Base: &authtypes.BaseAccount{ + Address: "test", + PubKey: nil, + AccountNumber: 10, + Sequence: 20, + }, + }, + } + + noInfo = &mockRetroCompatAccount{ + retroCompat: &authtypes.QueryLegacyAccountResponse{ + Account: &codectypes.Any{}, + }, + } + noImplement = &mockRetroCompatAccount{ + retroCompat: nil, + } +) + +func newMockRetroCompatAccount(name string, acc accountstd.Interface) accountstd.AccountCreatorFunc { + return func(_ accountstd.Dependencies) (string, accountstd.Interface, error) { + _, ok := acc.(*mockRetroCompatAccount) + if !ok { + return name, nil, errors.New("invalid account type") + } + return name, acc, nil + } +} + +func ProvideMockRetroCompatAccountValid() accountstd.DepinjectAccount { + return accountstd.DepinjectAccount{MakeAccount: newMockRetroCompatAccount("valid", valid)} +} + +func ProvideMockRetroCompatAccountNoInfo() accountstd.DepinjectAccount { + return accountstd.DepinjectAccount{MakeAccount: newMockRetroCompatAccount("no_info", noInfo)} +} + +func ProvideMockRetroCompatAccountNoImplement() accountstd.DepinjectAccount { + return accountstd.DepinjectAccount{MakeAccount: newMockRetroCompatAccount("no_implement", noImplement)} +} + +func (m mockRetroCompatAccount) RegisterInitHandler(builder *accountstd.InitBuilder) { + accountstd.RegisterInitHandler(builder, func(ctx context.Context, req *gogotypes.Empty) (*gogotypes.Empty, error) { + return &gogotypes.Empty{}, nil + }) +} + +func (m mockRetroCompatAccount) RegisterExecuteHandlers(_ *accountstd.ExecuteBuilder) {} + +func (m mockRetroCompatAccount) RegisterQueryHandlers(builder *accountstd.QueryBuilder) { + if m.retroCompat == nil { + return + } + accountstd.RegisterQueryHandler(builder, func(ctx context.Context, req *authtypes.QueryLegacyAccount) (*authtypes.QueryLegacyAccountResponse, error) { + return m.retroCompat, nil + }) +} + +func TestAuthToAccountsGRPCCompat(t *testing.T) { + accs := map[string]accountstd.Interface{ + "valid": valid, + "no_info": noInfo, + "no_implement": noImplement, + } + + f := createTestSuite(t) + + // init three accounts + for n, a := range accs { + _, addr, err := f.accountsKeeper.Init(f.ctx, n, []byte("me"), &gogotypes.Empty{}, nil) + require.NoError(t, err) + a.(*mockRetroCompatAccount).address = addr + } + + qs := authkeeper.NewQueryServer(f.authKeeper) + + t.Run("account supports info and account query", func(t *testing.T) { + infoResp, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{ + Address: f.mustAddr(valid.address), + }) + require.NoError(t, err) + require.Equal(t, infoResp.Info, valid.retroCompat.Base) + + accountResp, err := qs.Account(f.ctx, &authtypes.QueryAccountRequest{ + Address: f.mustAddr(noInfo.address), + }) + require.NoError(t, err) + require.Equal(t, accountResp.Account, valid.retroCompat.Account) + }) + + t.Run("account only supports account query, not info", func(t *testing.T) { + _, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{ + Address: f.mustAddr(noInfo.address), + }) + require.Error(t, err) + require.Equal(t, status.Code(err), codes.NotFound) + + resp, err := qs.Account(f.ctx, &authtypes.QueryAccountRequest{ + Address: f.mustAddr(noInfo.address), + }) + require.NoError(t, err) + require.Equal(t, resp.Account, valid.retroCompat.Account) + }) + + t.Run("account does not support any retro compat", func(t *testing.T) { + _, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{ + Address: f.mustAddr(noImplement.address), + }) + require.Error(t, err) + require.Equal(t, status.Code(err), codes.NotFound) + + _, err = qs.Account(f.ctx, &authtypes.QueryAccountRequest{ + Address: f.mustAddr(noImplement.address), + }) + + require.Error(t, err) + require.Equal(t, status.Code(err), codes.NotFound) + }) +} + +func TestAccountsBaseAccountRetroCompat(t *testing.T) { + f := createTestSuite(t) + // init a base acc + anyPk, err := codectypes.NewAnyWithValue(secp256k1.GenPrivKey().PubKey()) + require.NoError(t, err) + + // we init two accounts. Account number should start with 4 + // since the first three accounts are fee_collector, bonded_tokens_pool, not_bonded_tokens_pool + // generated by init genesis plus one more genesis account, which make the current account number 4. + _, _, err = f.accountsKeeper.Init(f.ctx, "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil) + require.NoError(t, err) + + _, addr, err := f.accountsKeeper.Init(f.ctx, "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil) + require.NoError(t, err) + + // try to query it via auth + qs := authkeeper.NewQueryServer(f.authKeeper) + + r, err := qs.Account(f.ctx, &authtypes.QueryAccountRequest{ + Address: f.mustAddr(addr), + }) + require.NoError(t, err) + require.NotNil(t, r.Account) + + info, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{ + Address: f.mustAddr(addr), + }) + require.NoError(t, err) + require.NotNil(t, info.Info) + require.Equal(t, info.Info.PubKey, anyPk) + // Account number should be 5 + require.Equal(t, info.Info.AccountNumber, uint64(5)) +} diff --git a/tests/integration/v2/auth/app_test.go b/tests/integration/v2/auth/app_test.go new file mode 100644 index 000000000000..01aba394c808 --- /dev/null +++ b/tests/integration/v2/auth/app_test.go @@ -0,0 +1,132 @@ +package auth + +import ( + "context" + "testing" + + "cosmossdk.io/core/router" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/runtime/v2" + "cosmossdk.io/x/accounts" + basedepinject "cosmossdk.io/x/accounts/defaults/base/depinject" + accountsv1 "cosmossdk.io/x/accounts/v1" + _ "cosmossdk.io/x/bank" // import as blank for app wiring + bankkeeper "cosmossdk.io/x/bank/keeper" + banktypes "cosmossdk.io/x/bank/types" + _ "cosmossdk.io/x/consensus" // import as blank for app wiring + _ "cosmossdk.io/x/staking" // import as blank for app wirings + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/tests/integration/v2" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + _ "github.com/cosmos/cosmos-sdk/x/auth" // import as blank for app wiring + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + _ "github.com/cosmos/cosmos-sdk/x/auth/tx/config" // import as blank for app wiring`` + _ "github.com/cosmos/cosmos-sdk/x/auth/vesting" // import as blank for app wiring + _ "github.com/cosmos/cosmos-sdk/x/genutil" // import as blank for app wiring + "github.com/stretchr/testify/require" +) + +type suite struct { + app *integration.App + + cdc codec.Codec + ctx context.Context + + authKeeper authkeeper.AccountKeeper + accountsKeeper accounts.Keeper + bankKeeper bankkeeper.Keeper +} + +func (s suite) mustAddr(address []byte) string { + str, _ := s.authKeeper.AddressCodec().BytesToString(address) + return str +} + +func createTestSuite(t *testing.T) *suite { + t.Helper() + res := suite{} + + moduleConfigs := []configurator.ModuleOption{ + configurator.AccountsModule(), + configurator.AuthModule(), + configurator.BankModule(), + configurator.VestingModule(), + configurator.StakingModule(), + configurator.TxModule(), + configurator.ValidateModule(), + configurator.ConsensusModule(), + configurator.GenutilModule(), + } + + var err error + startupCfg := integration.DefaultStartUpConfig(t) + + msgRouterService := integration.NewRouterService() + res.registerMsgRouterService(msgRouterService) + + var routerFactory runtime.RouterServiceFactory = func(_ []byte) router.Service { + return msgRouterService + } + + queryRouterService := integration.NewRouterService() + res.registerQueryRouterService(queryRouterService) + + serviceBuilder := runtime.NewRouterBuilder(routerFactory, queryRouterService) + + startupCfg.BranchService = &integration.BranchService{} + startupCfg.RouterServiceBuilder = serviceBuilder + + res.app, err = integration.NewApp( + depinject.Configs(configurator.NewAppV2Config(moduleConfigs...), depinject.Provide( + // inject desired account types: + basedepinject.ProvideAccount, + + // provide base account options + basedepinject.ProvideSecp256K1PubKey, + + // provide extra accounts + ProvideMockRetroCompatAccountValid, + ProvideMockRetroCompatAccountNoInfo, + ProvideMockRetroCompatAccountNoImplement, + ), depinject.Supply(log.NewNopLogger())), + startupCfg, + &res.bankKeeper, &res.accountsKeeper, &res.authKeeper) + require.NoError(t, err) + + res.ctx = res.app.StateLatestContext(t) + + return &res +} + +func (s *suite) registerMsgRouterService(router *integration.RouterService) { + // register custom router service + bankSendHandler := func(ctx context.Context, req transaction.Msg) (transaction.Msg, error) { + msg, ok := req.(*banktypes.MsgSend) + if !ok { + return nil, integration.ErrInvalidMsgType + } + msgServer := bankkeeper.NewMsgServerImpl(s.bankKeeper) + resp, err := msgServer.Send(ctx, msg) + return resp, err + } + + router.RegisterHandler(bankSendHandler, "cosmos.bank.v1beta1.MsgSend") +} + +func (s *suite) registerQueryRouterService(router *integration.RouterService) { + // register custom router service + queryHandler := func(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) { + req, ok := msg.(*accountsv1.AccountNumberRequest) + if !ok { + return nil, integration.ErrInvalidMsgType + } + qs := accounts.NewQueryServer(s.accountsKeeper) + resp, err := qs.AccountNumber(ctx, req) + return resp, err + } + + router.RegisterHandler(queryHandler, "cosmos.accounts.v1.AccountNumberRequest") +} diff --git a/tests/integration/v2/auth/migrate_x_accounts_test.go b/tests/integration/v2/auth/migrate_x_accounts_test.go new file mode 100644 index 000000000000..2e525fba7ba7 --- /dev/null +++ b/tests/integration/v2/auth/migrate_x_accounts_test.go @@ -0,0 +1,100 @@ +package auth + +import ( + "testing" + + "github.com/stretchr/testify/require" + + basev1 "cosmossdk.io/x/accounts/defaults/base/v1" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +func TestMigrateToAccounts(t *testing.T) { + f := createTestSuite(t) + + // create a module account + modAcc := &authtypes.ModuleAccount{ + BaseAccount: &authtypes.BaseAccount{ + Address: f.mustAddr([]byte("cookies")), + PubKey: nil, + AccountNumber: 0, + Sequence: 0, + }, + Name: "cookies", + Permissions: nil, + } + updatedMod := f.authKeeper.NewAccount(f.ctx, modAcc) + f.authKeeper.SetAccount(f.ctx, updatedMod) + + // create account + msgSrv := authkeeper.NewMsgServerImpl(f.authKeeper) + privKey := secp256k1.GenPrivKey() + addr := sdk.AccAddress(privKey.PubKey().Address()) + + acc := f.authKeeper.NewAccountWithAddress(f.ctx, addr) + require.NoError(t, acc.SetPubKey(privKey.PubKey())) + f.authKeeper.SetAccount(f.ctx, acc) + + t.Run("account does not exist", func(t *testing.T) { + resp, err := msgSrv.MigrateAccount(f.ctx, &authtypes.MsgMigrateAccount{ + Signer: f.mustAddr([]byte("notexist")), + AccountType: "base", + AccountInitMsg: nil, + }) + require.Nil(t, resp) + require.ErrorIs(t, err, sdkerrors.ErrUnknownAddress) + }) + + t.Run("invalid account type", func(t *testing.T) { + resp, err := msgSrv.MigrateAccount(f.ctx, &authtypes.MsgMigrateAccount{ + Signer: f.mustAddr(updatedMod.GetAddress()), + AccountType: "base", + AccountInitMsg: nil, + }) + require.Nil(t, resp) + require.ErrorContains(t, err, "only BaseAccount can be migrated") + }) + + t.Run("success", func(t *testing.T) { + pk, err := codectypes.NewAnyWithValue(privKey.PubKey()) + require.NoError(t, err) + + migrateMsg := &basev1.MsgInit{ + PubKey: pk, + InitSequence: 100, + } + + initMsgAny, err := codectypes.NewAnyWithValue(migrateMsg) + require.NoError(t, err) + + resp, err := msgSrv.MigrateAccount(f.ctx, &authtypes.MsgMigrateAccount{ + Signer: f.mustAddr(addr), + AccountType: "base", + AccountInitMsg: initMsgAny, + }) + require.NoError(t, err) + + // check response semantics. + require.Equal(t, resp.InitResponse.TypeUrl, "/cosmos.accounts.defaults.base.v1.MsgInitResponse") + require.NotNil(t, resp.InitResponse.Value) + + // check the account was removed from x/auth and added to x/accounts + require.Nil(t, f.authKeeper.GetAccount(f.ctx, addr)) + require.True(t, f.accountsKeeper.IsAccountsModuleAccount(f.ctx, addr)) + + // check the init information is correctly propagated. + seq, err := f.accountsKeeper.Query(f.ctx, addr, &basev1.QuerySequence{}) + require.NoError(t, err) + require.Equal(t, migrateMsg.InitSequence, seq.(*basev1.QuerySequenceResponse).Sequence) + + pkResp, err := f.accountsKeeper.Query(f.ctx, addr, &basev1.QueryPubKey{}) + require.NoError(t, err) + require.Equal(t, migrateMsg.PubKey, pkResp.(*basev1.QueryPubKeyResponse).PubKey) + }) +} diff --git a/tests/integration/v2/auth/msg_server_test.go b/tests/integration/v2/auth/msg_server_test.go new file mode 100644 index 000000000000..1a3d66f80c24 --- /dev/null +++ b/tests/integration/v2/auth/msg_server_test.go @@ -0,0 +1,161 @@ +package auth + +import ( + "context" + "fmt" + "strings" + "testing" + + "gotest.tools/v3/assert" + + "cosmossdk.io/core/transaction" + sdkmath "cosmossdk.io/math" + "cosmossdk.io/x/bank/testutil" + banktypes "cosmossdk.io/x/bank/types" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/tests/integration/v2" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +func TestAsyncExec(t *testing.T) { + t.Parallel() + s := createTestSuite(t) + + addrs := simtestutil.CreateIncrementalAccounts(2) + coins := sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(10))) + + assert.NilError(t, testutil.FundAccount(s.ctx, s.bankKeeper, addrs[0], sdk.NewCoins(sdk.NewInt64Coin("stake", 500)))) + + msg := &banktypes.MsgSend{ + FromAddress: addrs[0].String(), + ToAddress: addrs[1].String(), + Amount: coins, + } + msg2 := &banktypes.MsgSend{ + FromAddress: addrs[1].String(), + ToAddress: addrs[0].String(), + Amount: coins, + } + failingMsg := &banktypes.MsgSend{ + FromAddress: addrs[0].String(), + ToAddress: addrs[1].String(), + Amount: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.ZeroInt())), // No amount specified + } + + msgAny, err := codectypes.NewAnyWithValue(msg) + assert.NilError(t, err) + + msgAny2, err := codectypes.NewAnyWithValue(msg2) + assert.NilError(t, err) + + failingMsgAny, err := codectypes.NewAnyWithValue(failingMsg) + assert.NilError(t, err) + + testCases := []struct { + name string + req *authtypes.MsgNonAtomicExec + expectErr bool + expErrMsg string + }{ + { + name: "empty signer address", + req: &authtypes.MsgNonAtomicExec{ + Signer: "", + Msgs: []*codectypes.Any{}, + }, + expectErr: true, + expErrMsg: "empty signer address string is not allowed", + }, + { + name: "invalid signer address", + req: &authtypes.MsgNonAtomicExec{ + Signer: "invalid", + Msgs: []*codectypes.Any{}, + }, + expectErr: true, + expErrMsg: "invalid signer address", + }, + { + name: "empty msgs", + req: &authtypes.MsgNonAtomicExec{ + Signer: addrs[0].String(), + Msgs: []*codectypes.Any{}, + }, + expectErr: true, + expErrMsg: "messages cannot be empty", + }, + { + name: "valid msg", + req: &authtypes.MsgNonAtomicExec{ + Signer: addrs[0].String(), + Msgs: []*codectypes.Any{msgAny}, + }, + expectErr: false, + }, + { + name: "multiple messages being executed", + req: &authtypes.MsgNonAtomicExec{ + Signer: addrs[0].String(), + Msgs: []*codectypes.Any{msgAny, msgAny}, + }, + expectErr: false, + }, + { + name: "multiple messages with different signers", + req: &authtypes.MsgNonAtomicExec{ + Signer: addrs[0].String(), + Msgs: []*codectypes.Any{msgAny, msgAny2}, + }, + expectErr: false, + expErrMsg: "unauthorized: sender does not match expected sender", + }, + { + name: "multi msg with one failing being executed", + req: &authtypes.MsgNonAtomicExec{ + Signer: addrs[0].String(), + Msgs: []*codectypes.Any{msgAny, failingMsgAny}, + }, + expectErr: false, + expErrMsg: "invalid coins", + }, + } + + msgServer := authkeeper.NewMsgServerImpl(s.authKeeper) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := s.app.RunMsg( + t, + s.ctx, + func(ctx context.Context) (transaction.Msg, error) { + resp, e := msgServer.NonAtomicExec(ctx, tc.req) + return resp, e + }, + integration.WithAutomaticCommit(), + ) + if tc.expectErr { + assert.ErrorContains(t, err, tc.expErrMsg) + } else { + assert.NilError(t, err) + assert.Assert(t, res != nil) + + // check the result + result, ok := res.(*authtypes.MsgNonAtomicExecResponse) + assert.Assert(t, ok) + + if tc.expErrMsg != "" { + for _, res := range result.Results { + if res.Error != "" { + assert.Assert(t, strings.Contains(res.Error, tc.expErrMsg), fmt.Sprintf("res.Error %s does not contain %s", res.Error, tc.expErrMsg)) + } + continue + } + } + } + }) + } +} diff --git a/tests/integration/v2/services.go b/tests/integration/v2/services.go index f69aa70574a0..56dbf63341e5 100644 --- a/tests/integration/v2/services.go +++ b/tests/integration/v2/services.go @@ -2,17 +2,24 @@ package integration import ( "context" + "errors" "fmt" + gogoproto "github.com/cosmos/gogoproto/proto" + + "cosmossdk.io/core/branch" "cosmossdk.io/core/comet" "cosmossdk.io/core/event" "cosmossdk.io/core/gas" + "cosmossdk.io/core/router" "cosmossdk.io/core/server" corestore "cosmossdk.io/core/store" "cosmossdk.io/core/transaction" stfgas "cosmossdk.io/server/v2/stf/gas" ) +var ErrInvalidMsgType = fmt.Errorf("invalid message type") + func (c cometServiceImpl) CometInfo(context.Context) comet.Info { return comet.Info{} } @@ -117,3 +124,76 @@ func (e *eventManager) Emit(event transaction.Msg) error { func (e *eventManager) EmitKV(eventType string, attrs ...event.Attribute) error { return nil } + +var _ branch.Service = &BranchService{} + +// custom branch service for integration tests +type BranchService struct { +} + +func (bs *BranchService) Execute(ctx context.Context, f func(ctx context.Context) error) error { + _, ok := ctx.Value(contextKey).(*integrationContext) + if !ok { + return errors.New("context is not an integration context") + } + + return f(ctx) +} + +func (bs *BranchService) ExecuteWithGasLimit( + ctx context.Context, + gasLimit uint64, + f func(ctx context.Context) error, +) (gasUsed uint64, err error) { + iCtx, ok := ctx.Value(contextKey).(*integrationContext) + if !ok { + return 0, errors.New("context is not an integration context") + } + + // execute branched, with predefined gas limit. + err = f(ctx) + // restore original context + gasUsed = iCtx.gasMeter.Limit() - iCtx.gasMeter.Remaining() + _ = iCtx.gasMeter.Consume(gasUsed, "execute-with-gas-limit") + + return gasUsed, err +} + +// msgTypeURL returns the TypeURL of a proto message. +func msgTypeURL(msg gogoproto.Message) string { + return gogoproto.MessageName(msg) +} + +type routerHandler func(context.Context, transaction.Msg) (transaction.Msg, error) + +var _ router.Service = &RouterService{} + +// custom router service for integration tests +type RouterService struct { + handlers map[string]routerHandler +} + +func NewRouterService() *RouterService { + return &RouterService{ + handlers: make(map[string]routerHandler), + } +} + +func (rs *RouterService) RegisterHandler(handler routerHandler, typeUrl string) { + rs.handlers[typeUrl] = handler +} + +func (rs RouterService) CanInvoke(ctx context.Context, typeUrl string) error { + if rs.handlers[typeUrl] == nil { + return fmt.Errorf("no handler for typeURL %s", typeUrl) + } + return nil +} + +func (rs RouterService) Invoke(ctx context.Context, req transaction.Msg) (transaction.Msg, error) { + typeUrl := msgTypeURL(req) + if rs.handlers[typeUrl] == nil { + return nil, fmt.Errorf("no handler for typeURL %s", typeUrl) + } + return rs.handlers[typeUrl](ctx, req) +}