From abbe7f3ae9fbe234a3580848a25a1ca9ac964256 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 00:07:21 +0200 Subject: [PATCH] feat: int in-place testnet creation (backport #4297) (#4338) * feat: int in-place testnet creation (#4297) (cherry picked from commit f7618d3e217e1e8214ec183e0e6a12f63160deb4) # Conflicts: # docs/docs/03-CLI-Commands/01-cli-commands.md # ignite/pkg/chaincmd/chaincmd.go * updates * cl * re * consumer handling --------- Co-authored-by: Duong NV | Decentrio Co-authored-by: Julien Robert --- changelog.md | 4 + ignite/cmd/cmd.go | 1 + ignite/cmd/testnet.go | 24 ++ ignite/cmd/testnet_inplace.go | 129 +++++++++ ignite/pkg/chaincmd/chaincmd.go | 5 + ignite/pkg/chaincmd/in-place-testnet.go | 47 +++ ignite/pkg/chaincmd/runner/chain.go | 12 + ignite/services/chain/runtime.go | 11 + ignite/services/chain/testnet.go | 37 +++ ignite/templates/app/files/app/app.go.plush | 2 +- .../cmd/commands.go.plush | 3 +- .../cmd/testnet.go.plush | 274 ++++++++++++++++++ ignite/templates/app/files/go.mod.plush | 2 + 13 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 ignite/cmd/testnet.go create mode 100644 ignite/cmd/testnet_inplace.go create mode 100644 ignite/pkg/chaincmd/in-place-testnet.go create mode 100644 ignite/services/chain/testnet.go create mode 100644 ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet.go.plush diff --git a/changelog.md b/changelog.md index d2e5ac4b6d..84ed94d2b9 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- [#4297](https://github.com/ignite/cli/pull/4297) Add in-place testnet creation command for apps. + ### Changes - [#4292](https://github.com/ignite/cli/pull/4292) Bump Cosmos SDK to `v0.50.9` diff --git a/ignite/cmd/cmd.go b/ignite/cmd/cmd.go index a62e61d431..92e0133ee4 100644 --- a/ignite/cmd/cmd.go +++ b/ignite/cmd/cmd.go @@ -81,6 +81,7 @@ To get started, create a blockchain: NewApp(), NewDoctor(), NewCompletionCmd(), + NewTestnet(), ) c.AddCommand(deprecated()...) c.SetContext(ctx) diff --git a/ignite/cmd/testnet.go b/ignite/cmd/testnet.go new file mode 100644 index 0000000000..03e0068c06 --- /dev/null +++ b/ignite/cmd/testnet.go @@ -0,0 +1,24 @@ +package ignitecmd + +import ( + "github.com/spf13/cobra" +) + +// NewTestnet returns a command that groups scaffolding related sub commands. +func NewTestnet() *cobra.Command { + c := &cobra.Command{ + Use: "testnet [command]", + Short: "Start a testnet local", + Long: `Start a testnet local + +`, + Aliases: []string{"s"}, + Args: cobra.ExactArgs(1), + } + + c.AddCommand( + NewTestnetInPlace(), + ) + + return c +} diff --git a/ignite/cmd/testnet_inplace.go b/ignite/cmd/testnet_inplace.go new file mode 100644 index 0000000000..afccd3fa5f --- /dev/null +++ b/ignite/cmd/testnet_inplace.go @@ -0,0 +1,129 @@ +package ignitecmd + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/spf13/cobra" + + "github.com/ignite/cli/v28/ignite/pkg/cliui" + "github.com/ignite/cli/v28/ignite/pkg/cosmosaccount" + "github.com/ignite/cli/v28/ignite/pkg/errors" + "github.com/ignite/cli/v28/ignite/services/chain" +) + +func NewTestnetInPlace() *cobra.Command { + c := &cobra.Command{ + Use: "in-place", + Short: "Create and start a testnet from current local net state", + Long: `Testnet in-place command is used to create and start a testnet from current local net state(including mainnet). +After using this command in the repo containing the config.yml file, the network will start. +We can create a testnet from the local network state and mint additional coins for the desired accounts from the config.yml file.`, + Args: cobra.NoArgs, + RunE: testnetInPlaceHandler, + } + flagSetPath(c) + flagSetClearCache(c) + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetCheckDependencies()) + c.Flags().AddFlagSet(flagSetSkipProto()) + c.Flags().AddFlagSet(flagSetVerbose()) + + c.Flags().Bool(flagQuitOnFail, false, "quit program if the app fails to start") + return c +} + +func testnetInPlaceHandler(cmd *cobra.Command, _ []string) error { + session := cliui.New( + cliui.WithVerbosity(getVerbosity(cmd)), + ) + defer session.End() + + // Otherwise run the serve command directly + return testnetInplace(cmd, session) +} + +func testnetInplace(cmd *cobra.Command, session *cliui.Session) error { + chainOption := []chain.Option{ + chain.WithOutputer(session), + chain.CollectEvents(session.EventBus()), + chain.CheckCosmosSDKVersion(), + } + + if flagGetCheckDependencies(cmd) { + chainOption = append(chainOption, chain.CheckDependencies()) + } + + // check if custom config is defined + config, _ := cmd.Flags().GetString(flagConfig) + if config != "" { + chainOption = append(chainOption, chain.ConfigFile(config)) + } + + c, err := chain.NewWithHomeFlags(cmd, chainOption...) + if err != nil { + return err + } + + cfg, err := c.Config() + if err != nil { + return err + } + home, err := c.Home() + if err != nil { + return err + } + keyringBackend, err := c.KeyringBackend() + if err != nil { + return err + } + ca, err := cosmosaccount.New( + cosmosaccount.WithKeyringBackend(cosmosaccount.KeyringBackend(keyringBackend)), + cosmosaccount.WithHome(home), + ) + if err != nil { + return err + } + + var ( + operatorAddress sdk.ValAddress + accounts string + accErr *cosmosaccount.AccountDoesNotExistError + ) + for _, acc := range cfg.Accounts { + sdkAcc, err := ca.GetByName(acc.Name) + if errors.As(err, &accErr) { + sdkAcc, _, err = ca.Create(acc.Name) + } + if err != nil { + return err + } + + sdkAddr, err := sdkAcc.Address(getAddressPrefix(cmd)) + if err != nil { + return err + } + if len(cfg.Validators) == 0 { + return errors.Errorf("no validators found for account %s", sdkAcc.Name) + } + if cfg.Validators[0].Name == acc.Name { + accAddr, err := sdk.AccAddressFromBech32(sdkAddr) + if err != nil { + return err + } + operatorAddress = sdk.ValAddress(accAddr) + } + accounts = accounts + "," + sdkAddr + } + + chainID, err := c.ID() + if err != nil { + return err + } + + args := chain.InPlaceArgs{ + NewChainID: chainID, + NewOperatorAddress: operatorAddress.String(), + AccountsToFund: accounts, + } + return c.TestnetInPlace(cmd.Context(), args) +} diff --git a/ignite/pkg/chaincmd/chaincmd.go b/ignite/pkg/chaincmd/chaincmd.go index 2f15982c40..5f8f6cd984 100644 --- a/ignite/pkg/chaincmd/chaincmd.go +++ b/ignite/pkg/chaincmd/chaincmd.go @@ -27,6 +27,7 @@ const ( commandUnsafeReset = "unsafe-reset-all" commandExport = "export" commandTendermint = "tendermint" + commandTestnetInPlace = "in-place-testnet" optionHome = "--home" optionNode = "--node" @@ -53,6 +54,10 @@ const ( optionVestingAmount = "--vesting-amount" optionVestingEndTime = "--vesting-end-time" optionBroadcastMode = "--broadcast-mode" + optionAccount = "--account" + optionValidatorPrivateKey = "--validator-privkey" + optionAccountToFund = "--accounts-to-fund" + optionSkipConfirmation = "--skip-confirmation" constTendermint = "tendermint" constJSON = "json" diff --git a/ignite/pkg/chaincmd/in-place-testnet.go b/ignite/pkg/chaincmd/in-place-testnet.go new file mode 100644 index 0000000000..94ddeede23 --- /dev/null +++ b/ignite/pkg/chaincmd/in-place-testnet.go @@ -0,0 +1,47 @@ +package chaincmd + +import ( + "github.com/ignite/cli/v28/ignite/pkg/cmdrunner/step" +) + +type InPlaceOption func([]string) []string + +func InPlaceWithPrvKey(prvKey string) InPlaceOption { + return func(s []string) []string { + if len(prvKey) > 0 { + return append(s, optionValidatorPrivateKey, prvKey) + } + return s + } +} + +func InPlaceWithAccountToFund(accounts string) InPlaceOption { + return func(s []string) []string { + if len(accounts) > 0 { + return append(s, optionAccountToFund, accounts) + } + return s + } +} + +func InPlaceWithSkipConfirmation() InPlaceOption { + return func(s []string) []string { + return append(s, optionSkipConfirmation) + } +} + +// TestnetInPlaceCommand return command to start testnet in-place. +func (c ChainCmd) TestnetInPlaceCommand(newChainID, newOperatorAddress string, options ...InPlaceOption) step.Option { + command := []string{ + commandTestnetInPlace, + newChainID, + newOperatorAddress, + } + + // Apply the options provided by the user + for _, apply := range options { + command = apply(command) + } + + return c.daemonCommand(command) +} diff --git a/ignite/pkg/chaincmd/runner/chain.go b/ignite/pkg/chaincmd/runner/chain.go index 1213968186..d863d12cc3 100644 --- a/ignite/pkg/chaincmd/runner/chain.go +++ b/ignite/pkg/chaincmd/runner/chain.go @@ -45,6 +45,18 @@ func NewKV(key, value string) KV { var gentxRe = regexp.MustCompile(`(?m)"(.+?)"`) +func (r Runner) InPlace(ctx context.Context, newChainID, newOperatorAddress string, options ...chaincmd.InPlaceOption) error { + runOptions := runOptions{ + stdout: os.Stdout, + stderr: os.Stderr, + } + return r.run( + ctx, + runOptions, + r.chainCmd.TestnetInPlaceCommand(newChainID, newOperatorAddress, options...), + ) +} + // Gentx generates a genesis tx carrying a self delegation. func (r Runner) Gentx( ctx context.Context, diff --git a/ignite/services/chain/runtime.go b/ignite/services/chain/runtime.go index 8e44db7d84..0e45af0295 100644 --- a/ignite/services/chain/runtime.go +++ b/ignite/services/chain/runtime.go @@ -36,6 +36,17 @@ func (c Chain) Gentx(ctx context.Context, runner chaincmdrunner.Runner, v Valida ) } +func (c Chain) InPlace(ctx context.Context, runner chaincmdrunner.Runner, args InPlaceArgs) error { + err := runner.InPlace(ctx, + args.NewChainID, + args.NewOperatorAddress, + chaincmd.InPlaceWithPrvKey(args.PrvKeyValidator), + chaincmd.InPlaceWithAccountToFund(args.AccountsToFund), + chaincmd.InPlaceWithSkipConfirmation(), + ) + return err +} + // Start wraps the "appd start" command to begin running a chain from the daemon. func (c Chain) Start(ctx context.Context, runner chaincmdrunner.Runner, cfg *chainconfig.Config) error { validator, err := chainconfig.FirstValidator(cfg) diff --git a/ignite/services/chain/testnet.go b/ignite/services/chain/testnet.go new file mode 100644 index 0000000000..c75c65e688 --- /dev/null +++ b/ignite/services/chain/testnet.go @@ -0,0 +1,37 @@ +package chain + +import ( + "context" + "os" + + chainconfig "github.com/ignite/cli/v28/ignite/config/chain" +) + +type InPlaceArgs struct { + NewChainID string + NewOperatorAddress string + PrvKeyValidator string + AccountsToFund string +} + +func (c Chain) TestnetInPlace(ctx context.Context, args InPlaceArgs) error { + commands, err := c.Commands(ctx) + if err != nil { + return err + } + + // make sure that config.yml exists + if c.options.ConfigFile != "" { + if _, err := os.Stat(c.options.ConfigFile); err != nil { + return err + } + } else if _, err := chainconfig.LocateDefault(c.app.Path); err != nil { + return err + } + + err = c.InPlace(ctx, commands, args) + if err != nil { + return err + } + return nil +} diff --git a/ignite/templates/app/files/app/app.go.plush b/ignite/templates/app/files/app/app.go.plush index 0d3026c9e9..d93a7de726 100644 --- a/ignite/templates/app/files/app/app.go.plush +++ b/ignite/templates/app/files/app/app.go.plush @@ -404,4 +404,4 @@ func BlockedAddresses() map[string]bool { } } return result -} +} \ No newline at end of file diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush index 685899a07d..5388a9d8d7 100644 --- a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/commands.go.plush @@ -35,6 +35,7 @@ func initRootCmd( ) { rootCmd.AddCommand( genutilcli.InitCmd(basicManager, app.DefaultNodeHome), + NewTestnetCmd(addModuleInitFlags), debug.Cmd(), confixcmd.ConfigCommand(), pruning.Cmd(newApp, app.DefaultNodeHome), @@ -186,4 +187,4 @@ func appExport( } return bApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) -} \ No newline at end of file +} diff --git a/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet.go.plush b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet.go.plush new file mode 100644 index 0000000000..73b723892e --- /dev/null +++ b/ignite/templates/app/files/cmd/{{binaryNamePrefix}}d/cmd/testnet.go.plush @@ -0,0 +1,274 @@ +package cmd + +import ( + "fmt" + "io" + "strings" + + "cosmossdk.io/log" + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/libs/bytes" + tmos "github.com/cometbft/cometbft/libs/os" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/client/flags" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/spf13/cast" + "github.com/spf13/cobra" + + "<%= ModulePath %>/app" +) + +const ( + valVotingPower int64 = 900000000000000 +) + +var ( + flagAccountsToFund = "accounts-to-fund" +) + +type valArgs struct { + newValAddr bytes.HexBytes + newOperatorAddress string + newValPubKey crypto.PubKey + accountsToFund []sdk.AccAddress + upgradeToTrigger string + homeDir string +} + +func NewTestnetCmd(addStartFlags servertypes.ModuleInitFlags) *cobra.Command { + cmd := server.InPlaceTestnetCreator(newTestnetApp) + addStartFlags(cmd) + cmd.Short = "Updates chain's application and consensus state with provided validator info and starts the node" + cmd.Long = `The test command modifies both application and consensus stores within a local mainnet node and starts the node, +with the aim of facilitating testing procedures. This command replaces existing validator data with updated information, +thereby removing the old validator set and introducing a new set suitable for local testing purposes. By altering the state extracted from the mainnet node, +it enables developers to configure their local environments to reflect mainnet conditions more accurately.` + + cmd.Example = fmt.Sprintf(`%sd in-place-testnet testing-1 cosmosvaloper1w7f3xx7e75p4l7qdym5msqem9rd4dyc4mq79dm --home $HOME/.%sd/validator1 --validator-privkey=6dq+/KHNvyiw2TToCgOpUpQKIzrLs69Rb8Az39xvmxPHNoPxY1Cil8FY+4DhT9YwD6s0tFABMlLcpaylzKKBOg== --accounts-to-fund="cosmos1f7twgcq4ypzg7y24wuywy06xmdet8pc4473tnq,cosmos1qvuhm5m644660nd8377d6l7yz9e9hhm9evmx3x"`, "<%= ModulePath %>", "<%= ModulePath %>") + + cmd.Flags().String(flagAccountsToFund, "", "Comma-separated list of account addresses that will be funded for testing purposes") + return cmd +} + +// newTestnetApp starts by running the normal newApp method. From there, the app interface returned is modified in order +// for a testnet to be created from the provided app. +func newTestnetApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts servertypes.AppOptions) servertypes.Application { + // Create an app and type cast to an App + newApp := newApp(logger, db, traceStore, appOpts) + testApp, ok := newApp.(*app.App) + if !ok { + panic("app created from newApp is not of type App") + } + + // Get command args + args, err := getCommandArgs(appOpts) + if err != nil { + panic(err) + } + + return initAppForTestnet(testApp, args) +} + +<%= if (!IsConsumerChain) { %> +func initAppForTestnet(app *app.App, args valArgs) *app.App { + // Required Changes: + // + ctx := app.BaseApp.NewUncachedContext(true, tmproto.Header{}) + + pubkey := &ed25519.PubKey{Key: args.newValPubKey.Bytes()} + pubkeyAny, err := codectypes.NewAnyWithValue(pubkey) + if err != nil { + tmos.Exit(err.Error()) + } + + // STAKING + // + + // Create Validator struct for our new validator. + newVal := stakingtypes.Validator{ + OperatorAddress: args.newOperatorAddress, + ConsensusPubkey: pubkeyAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(valVotingPower), + DelegatorShares: math.LegacyMustNewDecFromStr("10000000"), + Description: stakingtypes.Description{ + Moniker: "Testnet Validator", + }, + Commission: stakingtypes.Commission{ + CommissionRates: stakingtypes.CommissionRates{ + Rate: math.LegacyMustNewDecFromStr("0.05"), + MaxRate: math.LegacyMustNewDecFromStr("0.1"), + MaxChangeRate: math.LegacyMustNewDecFromStr("0.05"), + }, + }, + MinSelfDelegation: math.OneInt(), + } + + validator, err := app.StakingKeeper.ValidatorAddressCodec().StringToBytes(newVal.GetOperator()) + if err != nil { + tmos.Exit(err.Error()) + } + + // Remove all validators from power store + stakingKey := app.GetKey(stakingtypes.ModuleName) + stakingStore := ctx.KVStore(stakingKey) + iterator, err := app.StakingKeeper.ValidatorsPowerStoreIterator(ctx) + if err != nil { + tmos.Exit(err.Error()) + } + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all valdiators from last validators store + iterator, err = app.StakingKeeper.LastValidatorsIterator(ctx) + if err != nil { + tmos.Exit(err.Error()) + } + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all validators from validators store + iterator = stakingStore.Iterator(stakingtypes.ValidatorsKey, storetypes.PrefixEndBytes(stakingtypes.ValidatorsKey)) + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Remove all validators from unbonding queue + iterator = stakingStore.Iterator(stakingtypes.ValidatorQueueKey, storetypes.PrefixEndBytes(stakingtypes.ValidatorQueueKey)) + for ; iterator.Valid(); iterator.Next() { + stakingStore.Delete(iterator.Key()) + } + iterator.Close() + + // Add our validator to power and last validators store + app.StakingKeeper.SetValidator(ctx, newVal) + err = app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal) + if err != nil { + tmos.Exit(err.Error()) + } + app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal) + app.StakingKeeper.SetLastValidatorPower(ctx, validator, 0) + if err := app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, validator); err != nil { + tmos.Exit(err.Error()) + } + + // DISTRIBUTION + // + + // Initialize records for this validator across all distribution stores + app.DistrKeeper.SetValidatorHistoricalRewards(ctx, validator, 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1)) + app.DistrKeeper.SetValidatorCurrentRewards(ctx, validator, distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)) + app.DistrKeeper.SetValidatorAccumulatedCommission(ctx, validator, distrtypes.InitialValidatorAccumulatedCommission()) + app.DistrKeeper.SetValidatorOutstandingRewards(ctx, validator, distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) + +<%= if (!IsChainMinimal) { %> + // SLASHING + // + + // Set validator signing info for our new validator. + newConsAddr := sdk.ConsAddress(args.newValAddr.Bytes()) + newValidatorSigningInfo := slashingtypes.ValidatorSigningInfo{ + Address: newConsAddr.String(), + StartHeight: app.LastBlockHeight() - 1, + Tombstoned: false, + } + app.SlashingKeeper.SetValidatorSigningInfo(ctx, newConsAddr, newValidatorSigningInfo) +<% } %> + + // BANK + // + bondDenom, err := app.StakingKeeper.BondDenom(ctx) + if err != nil { + tmos.Exit(err.Error()) + } + + defaultCoins := sdk.NewCoins(sdk.NewInt64Coin(bondDenom, 1000000000)) + + // Fund local accounts + for _, account := range args.accountsToFund { + err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins) + if err != nil { + tmos.Exit(err.Error()) + } + err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins) + if err != nil { + tmos.Exit(err.Error()) + } + } + + return app +} +<% } else { %> +func initAppForTestnet(app *app.App, args valArgs) *app.App { + // wire export validator replacement + // wire add new validator + // wire send coints to new accounts + + return app +} +<% } %> + +// parse the input flags and returns valArgs +func getCommandArgs(appOpts servertypes.AppOptions) (valArgs, error) { + args := valArgs{} + + newValAddr, ok := appOpts.Get(server.KeyNewValAddr).(bytes.HexBytes) + if !ok { + panic("newValAddr is not of type bytes.HexBytes") + } + args.newValAddr = newValAddr + newValPubKey, ok := appOpts.Get(server.KeyUserPubKey).(crypto.PubKey) + if !ok { + panic("newValPubKey is not of type crypto.PubKey") + } + args.newValPubKey = newValPubKey + newOperatorAddress, ok := appOpts.Get(server.KeyNewOpAddr).(string) + if !ok { + panic("newOperatorAddress is not of type string") + } + args.newOperatorAddress = newOperatorAddress + upgradeToTrigger, ok := appOpts.Get(server.KeyTriggerTestnetUpgrade).(string) + if !ok { + panic("upgradeToTrigger is not of type string") + } + args.upgradeToTrigger = upgradeToTrigger + + // validate and set accounts to fund + accountsString := cast.ToString(appOpts.Get(flagAccountsToFund)) + + for _, account := range strings.Split(accountsString, ",") { + if account != "" { + addr, err := sdk.AccAddressFromBech32(account) + if err != nil { + return args, fmt.Errorf("invalid bech32 address format %w", err) + } + args.accountsToFund = append(args.accountsToFund, addr) + } + } + + // home dir + homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) + if homeDir == "" { + return args, fmt.Errorf("invalid home dir") + } + args.homeDir = homeDir + + return args, nil +} diff --git a/ignite/templates/app/files/go.mod.plush b/ignite/templates/app/files/go.mod.plush index 24df253da0..a493a54c4c 100644 --- a/ignite/templates/app/files/go.mod.plush +++ b/ignite/templates/app/files/go.mod.plush @@ -17,6 +17,7 @@ require ( cosmossdk.io/depinject v1.0.0 cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.3.1 + cosmossdk.io/math v1.3.0 cosmossdk.io/store v1.1.0 cosmossdk.io/tools/confix v0.1.1 cosmossdk.io/x/circuit v0.1.0 @@ -39,6 +40,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 + github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0