diff --git a/CHANGELOG.md b/CHANGELOG.md index 455b3caec312..00e23a10eae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i ### API Breaking Changes * (baseapp) [#21413](https://github.com/cosmos/cosmos-sdk/pull/21413) Add `SelectBy` method to `Mempool` interface, which is thread-safe to use. +* (x/genutil) [#21372](https://github.com/cosmos/cosmos-sdk/pull/21372) Remove `AddGenesisAccount` for `AddGenesisAccounts`. ### Deprecated diff --git a/x/genutil/client/cli/commands.go b/x/genutil/client/cli/commands.go index 6e1415fe0796..00042ec69b8d 100644 --- a/x/genutil/client/cli/commands.go +++ b/x/genutil/client/cli/commands.go @@ -39,6 +39,7 @@ func CommandsWithCustomMigrationMap(genutilModule genutil.AppModule, genMM genes CollectGenTxsCmd(genutilModule.GenTxValidator()), ValidateGenesisCmd(genMM), AddGenesisAccountCmd(), + AddBulkGenesisAccountCmd(), ExportCmd(appExport), ) diff --git a/x/genutil/client/cli/genaccount.go b/x/genutil/client/cli/genaccount.go index 34acef113e2e..938e711b3aca 100644 --- a/x/genutil/client/cli/genaccount.go +++ b/x/genutil/client/cli/genaccount.go @@ -2,7 +2,9 @@ package cli import ( "bufio" + "encoding/json" "fmt" + "os" "github.com/spf13/cobra" @@ -71,7 +73,33 @@ contain valid denominations. Accounts may optionally be supplied with vesting pa vestingAmtStr, _ := cmd.Flags().GetString(flagVestingAmt) moduleNameStr, _ := cmd.Flags().GetString(flagModuleName) - return genutil.AddGenesisAccount(clientCtx.Codec, clientCtx.AddressCodec, addr, appendflag, config.GenesisFile(), args[1], vestingAmtStr, vestingStart, vestingEnd, moduleNameStr) + addrStr, err := addressCodec.BytesToString(addr) + if err != nil { + return err + } + + coins, err := sdk.ParseCoinsNormalized(args[1]) + if err != nil { + return err + } + + vestingAmt, err := sdk.ParseCoinsNormalized(vestingAmtStr) + if err != nil { + return err + } + + accounts := []genutil.GenesisAccount{ + { + Address: addrStr, + Coins: coins, + VestingAmt: vestingAmt, + VestingStart: vestingStart, + VestingEnd: vestingEnd, + ModuleName: moduleNameStr, + }, + } + + return genutil.AddGenesisAccounts(clientCtx.Codec, clientCtx.AddressCodec, accounts, appendflag, config.GenesisFile()) }, } @@ -85,3 +113,66 @@ contain valid denominations. Accounts may optionally be supplied with vesting pa return cmd } + +// AddBulkGenesisAccountCmd returns bulk-add-genesis-account cobra Command. +// This command is provided as a default, applications are expected to provide their own command if custom genesis accounts are needed. +func AddBulkGenesisAccountCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "bulk-add-genesis-account [/file/path.json]", + Short: "Bulk add genesis accounts to genesis.json", + Example: `bulk-add-genesis-account accounts.json + +where accounts.json is: + +[ + { + "address": "cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5", + "coins": [ + { "denom": "umuon", "amount": "100000000" }, + { "denom": "stake", "amount": "200000000" } + ] + }, + { + "address": "cosmos1e0jnq2sun3dzjh8p2xq95kk0expwmd7shwjpfg", + "coins": [ + { "denom": "umuon", "amount": "500000000" } + ], + "vesting_amt": [ + { "denom": "umuon", "amount": "400000000" } + ], + "vesting_start": 1724711478, + "vesting_end": 1914013878 + } +] +`, + Long: `Add genesis accounts in bulk to genesis.json. The provided account must specify +the account address and a list of initial coins. The list of initial tokens must +contain valid denominations. Accounts may optionally be supplied with vesting parameters. +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx := client.GetClientContextFromCmd(cmd) + config := client.GetConfigFromCmd(cmd) + + f, err := os.Open(args[0]) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + var accounts []genutil.GenesisAccount + if err := json.NewDecoder(f).Decode(&accounts); err != nil { + return fmt.Errorf("failed to decode JSON: %w", err) + } + + appendflag, _ := cmd.Flags().GetBool(flagAppendMode) + + return genutil.AddGenesisAccounts(clientCtx.Codec, clientCtx.AddressCodec, accounts, appendflag, config.GenesisFile()) + }, + } + + cmd.Flags().Bool(flagAppendMode, false, "append the coins to an account already in the genesis.json file") + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/genutil/client/cli/genaccount_test.go b/x/genutil/client/cli/genaccount_test.go index c0b293cb43b3..c75894f3a2fc 100644 --- a/x/genutil/client/cli/genaccount_test.go +++ b/x/genutil/client/cli/genaccount_test.go @@ -2,6 +2,9 @@ package cli_test import ( "context" + "encoding/json" + "os" + "path" "testing" "github.com/spf13/viper" @@ -9,6 +12,7 @@ import ( corectx "cosmossdk.io/core/context" "cosmossdk.io/log" + banktypes "cosmossdk.io/x/bank/types" "github.com/cosmos/cosmos-sdk/client" codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" @@ -18,8 +22,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/genutil" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" ) func TestAddGenesisAccountCmd(t *testing.T) { @@ -111,3 +117,166 @@ func TestAddGenesisAccountCmd(t *testing.T) { }) } } + +func TestBulkAddGenesisAccountCmd(t *testing.T) { + ac := codectestutil.CodecOptions{}.GetAddressCodec() + _, _, addr1 := testdata.KeyTestPubAddr() + _, _, addr2 := testdata.KeyTestPubAddr() + _, _, addr3 := testdata.KeyTestPubAddr() + addr1Str, err := ac.BytesToString(addr1) + require.NoError(t, err) + addr2Str, err := ac.BytesToString(addr2) + require.NoError(t, err) + addr3Str, err := ac.BytesToString(addr3) + require.NoError(t, err) + + tests := []struct { + name string + state [][]genutil.GenesisAccount + expected map[string]sdk.Coins + appendFlag bool + expectErr bool + }{ + { + name: "invalid address", + state: [][]genutil.GenesisAccount{ + { + { + Address: "invalid", + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)), + }, + }, + }, + expectErr: true, + }, + { + name: "no append flag for multiple account adds", + state: [][]genutil.GenesisAccount{ + { + { + Address: addr1Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)), + }, + }, + { + { + Address: addr1Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 2)), + }, + }, + }, + appendFlag: false, + expectErr: true, + }, + + { + name: "multiple additions with append", + state: [][]genutil.GenesisAccount{ + { + { + Address: addr1Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)), + }, + { + Address: addr2Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)), + }, + }, + { + { + Address: addr1Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 2)), + }, + { + Address: addr2Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("stake", 1)), + }, + { + Address: addr3Str, + Coins: sdk.NewCoins(sdk.NewInt64Coin("test", 1)), + }, + }, + }, + expected: map[string]sdk.Coins{ + addr1Str: sdk.NewCoins(sdk.NewInt64Coin("test", 3)), + addr2Str: sdk.NewCoins(sdk.NewInt64Coin("test", 1), sdk.NewInt64Coin("stake", 1)), + addr3Str: sdk.NewCoins(sdk.NewInt64Coin("test", 1)), + }, + appendFlag: true, + expectErr: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + home := t.TempDir() + logger := log.NewNopLogger() + v := viper.New() + + encodingConfig := moduletestutil.MakeTestEncodingConfig(codectestutil.CodecOptions{}, auth.AppModule{}) + appCodec := encodingConfig.Codec + txConfig := encodingConfig.TxConfig + err = genutiltest.ExecInitCmd(testMbm, home, appCodec) + require.NoError(t, err) + + err = writeAndTrackDefaultConfig(v, home) + require.NoError(t, err) + clientCtx := client.Context{}.WithCodec(appCodec).WithHomeDir(home). + WithAddressCodec(ac).WithTxConfig(txConfig) + + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, corectx.ViperContextKey, v) + ctx = context.WithValue(ctx, corectx.LoggerContextKey, logger) + + // The first iteration (pre-append) may not error. + // Check if any errors after all state transitions to genesis. + doesErr := false + + // apply multiple state iterations if applicable (e.g. --append) + for _, state := range tc.state { + bz, err := json.Marshal(state) + require.NoError(t, err) + + filePath := path.Join(home, "accounts.json") + err = os.WriteFile(filePath, bz, 0o600) + require.NoError(t, err) + + cmd := genutilcli.AddBulkGenesisAccountCmd() + args := []string{filePath} + if tc.appendFlag { + args = append(args, "--append") + } + cmd.SetArgs(args) + + err = cmd.ExecuteContext(ctx) + if err != nil { + doesErr = true + } + } + require.Equal(t, tc.expectErr, doesErr) + + // an error already occurred, no need to check the state + if doesErr { + return + } + + appState, _, err := genutiltypes.GenesisStateFromGenFile(path.Join(home, "config", "genesis.json")) + require.NoError(t, err) + + bankState := banktypes.GetGenesisStateFromAppState(encodingConfig.Codec, appState) + + require.EqualValues(t, len(tc.expected), len(bankState.Balances)) + for _, acc := range bankState.Balances { + require.True(t, tc.expected[acc.Address].Equal(acc.Coins), "expected: %v, got: %v", tc.expected[acc.Address], acc.Coins) + } + + expectedSupply := sdk.NewCoins() + for _, coins := range tc.expected { + expectedSupply = expectedSupply.Add(coins...) + } + require.Equal(t, expectedSupply, bankState.Supply) + }) + } +} diff --git a/x/genutil/genaccounts.go b/x/genutil/genaccounts.go index 75899c8cfd89..d3472fb792f6 100644 --- a/x/genutil/genaccounts.go +++ b/x/genutil/genaccounts.go @@ -15,133 +15,148 @@ import ( genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" ) -// AddGenesisAccount adds a genesis account to the genesis state. -// Where `cdc` is client codec, `genesisFileUrl` is the path/url of current genesis file, -// `accAddr` is the address to be added to the genesis state, `amountStr` is the list of initial coins -// to be added for the account, `appendAcct` updates the account if already exists. -// `vestingStart, vestingEnd and vestingAmtStr` respectively are the schedule start time, end time (unix epoch) -// `moduleName` is the module name for which the account is being created -// and coins to be appended to the account already in the genesis.json file. -func AddGenesisAccount( +type GenesisAccount struct { + // Base + Address string `json:"address"` + Coins sdk.Coins `json:"coins"` + + // Vesting + VestingAmt sdk.Coins `json:"vesting_amt,omitempty"` + VestingStart int64 `json:"vesting_start,omitempty"` + VestingEnd int64 `json:"vesting_end,omitempty"` + + // Module + ModuleName string `json:"module_name,omitempty"` +} + +// AddGenesisAccounts adds genesis accounts to the genesis state. +// Where `cdc` is the client codec, `addressCodec` is the address codec, `accounts` are the genesis accounts to add, +// `appendAcct` updates the account if already exists, and `genesisFileURL` is the path/url of the current genesis file. +func AddGenesisAccounts( cdc codec.Codec, addressCodec address.Codec, - accAddr sdk.AccAddress, + accounts []GenesisAccount, appendAcct bool, - genesisFileURL, amountStr, vestingAmtStr string, - vestingStart, vestingEnd int64, - moduleName string, + genesisFileURL string, ) error { - addr, err := addressCodec.BytesToString(accAddr) + appState, appGenesis, err := genutiltypes.GenesisStateFromGenFile(genesisFileURL) if err != nil { - return err + return fmt.Errorf("failed to unmarshal genesis state: %w", err) } - coins, err := sdk.ParseCoinsNormalized(amountStr) - if err != nil { - return fmt.Errorf("failed to parse coins: %w", err) - } + authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState) + bankGenState := banktypes.GetGenesisStateFromAppState(cdc, appState) - vestingAmt, err := sdk.ParseCoinsNormalized(vestingAmtStr) + accs, err := authtypes.UnpackAccounts(authGenState.Accounts) if err != nil { - return fmt.Errorf("failed to parse vesting amount: %w", err) + return fmt.Errorf("failed to get accounts from any: %w", err) } - // create concrete account type based on input parameters - var genAccount authtypes.GenesisAccount + newSupplyCoinsCache := sdk.NewCoins() + balanceCache := make(map[string]banktypes.Balance) + for _, acc := range accs { + for _, balance := range bankGenState.GetBalances() { + if balance.Address == acc.GetAddress().String() { + balanceCache[acc.GetAddress().String()] = balance + } + } + } - balances := banktypes.Balance{Address: addr, Coins: coins.Sort()} - baseAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0) + for _, acc := range accounts { + addr := acc.Address + coins := acc.Coins - if !vestingAmt.IsZero() { - baseVestingAccount, err := authvesting.NewBaseVestingAccount(baseAccount, vestingAmt.Sort(), vestingEnd) + accAddr, err := addressCodec.StringToBytes(addr) if err != nil { - return fmt.Errorf("failed to create base vesting account: %w", err) + return fmt.Errorf("failed to parse account address %s: %w", addr, err) } - if (balances.Coins.IsZero() && !baseVestingAccount.OriginalVesting.IsZero()) || - baseVestingAccount.OriginalVesting.IsAnyGT(balances.Coins) { - return errors.New("vesting amount cannot be greater than total amount") - } + // create concrete account type based on input parameters + var genAccount authtypes.GenesisAccount - switch { - case vestingStart != 0 && vestingEnd != 0: - genAccount = authvesting.NewContinuousVestingAccountRaw(baseVestingAccount, vestingStart) + balances := banktypes.Balance{Address: addr, Coins: coins.Sort()} + baseAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0) - case vestingEnd != 0: - genAccount = authvesting.NewDelayedVestingAccountRaw(baseVestingAccount) + vestingAmt := acc.VestingAmt + if !vestingAmt.IsZero() { + vestingStart := acc.VestingStart + vestingEnd := acc.VestingEnd - default: - return errors.New("invalid vesting parameters; must supply start and end time or end time") - } - } else if moduleName != "" { - genAccount = authtypes.NewEmptyModuleAccount(moduleName, authtypes.Burner, authtypes.Minter) - } else { - genAccount = baseAccount - } + baseVestingAccount, err := authvesting.NewBaseVestingAccount(baseAccount, vestingAmt.Sort(), vestingEnd) + if err != nil { + return fmt.Errorf("failed to create base vesting account: %w", err) + } - if err := genAccount.Validate(); err != nil { - return fmt.Errorf("failed to validate new genesis account: %w", err) - } + if (balances.Coins.IsZero() && !baseVestingAccount.OriginalVesting.IsZero()) || + baseVestingAccount.OriginalVesting.IsAnyGT(balances.Coins) { + return errors.New("vesting amount cannot be greater than total amount") + } - appState, appGenesis, err := genutiltypes.GenesisStateFromGenFile(genesisFileURL) - if err != nil { - return fmt.Errorf("failed to unmarshal genesis state: %w", err) - } + switch { + case vestingStart != 0 && vestingEnd != 0: + genAccount = authvesting.NewContinuousVestingAccountRaw(baseVestingAccount, vestingStart) - authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState) + case vestingEnd != 0: + genAccount = authvesting.NewDelayedVestingAccountRaw(baseVestingAccount) - accs, err := authtypes.UnpackAccounts(authGenState.Accounts) - if err != nil { - return fmt.Errorf("failed to get accounts from any: %w", err) - } + default: + return errors.New("invalid vesting parameters; must supply start and end time or end time") + } + } else if acc.ModuleName != "" { + genAccount = authtypes.NewEmptyModuleAccount(acc.ModuleName, authtypes.Burner, authtypes.Minter) + } else { + genAccount = baseAccount + } - bankGenState := banktypes.GetGenesisStateFromAppState(cdc, appState) - if accs.Contains(accAddr) { - if !appendAcct { - return fmt.Errorf(" Account %s already exists\nUse `append` flag to append account at existing address", accAddr) + if err := genAccount.Validate(); err != nil { + return fmt.Errorf("failed to validate new genesis account: %w", err) } - genesisB := banktypes.GetGenesisStateFromAppState(cdc, appState) - for idx, acc := range genesisB.Balances { - if acc.Address != addr { - continue + if _, ok := balanceCache[addr]; ok { + if !appendAcct { + return fmt.Errorf(" Account %s already exists\nUse `append` flag to append account at existing address", accAddr) } - updatedCoins := acc.Coins.Add(coins...) - bankGenState.Balances[idx] = banktypes.Balance{Address: addr, Coins: updatedCoins.Sort()} - break - } - } else { - // Add the new account to the set of genesis accounts and sanitize the accounts afterwards. - accs = append(accs, genAccount) - accs = authtypes.SanitizeGenesisAccounts(accs) + for idx, acc := range bankGenState.Balances { + if acc.Address != addr { + continue + } - genAccs, err := authtypes.PackAccounts(accs) - if err != nil { - return fmt.Errorf("failed to convert accounts into any's: %w", err) + updatedCoins := acc.Coins.Add(coins...) + bankGenState.Balances[idx] = banktypes.Balance{Address: addr, Coins: updatedCoins.Sort()} + break + } + } else { + accs = append(accs, genAccount) + bankGenState.Balances = append(bankGenState.Balances, balances) } - authGenState.Accounts = genAccs - authGenStateBz, err := cdc.MarshalJSON(&authGenState) - if err != nil { - return fmt.Errorf("failed to marshal auth genesis state: %w", err) - } - appState[authtypes.ModuleName] = authGenStateBz + newSupplyCoinsCache = newSupplyCoinsCache.Add(coins...) + } + + accs = authtypes.SanitizeGenesisAccounts(accs) - bankGenState.Balances = append(bankGenState.Balances, balances) + authGenState.Accounts, err = authtypes.PackAccounts(accs) + if err != nil { + return fmt.Errorf("failed to convert accounts into any's: %w", err) + } + + appState[authtypes.ModuleName], err = cdc.MarshalJSON(&authGenState) + if err != nil { + return fmt.Errorf("failed to marshal auth genesis state: %w", err) } bankGenState.Balances, err = banktypes.SanitizeGenesisBalances(bankGenState.Balances, addressCodec) if err != nil { - return fmt.Errorf("failed to sanitize genesis balance: %w", err) + return fmt.Errorf("failed to sanitize genesis bank Balances: %w", err) } - bankGenState.Supply = bankGenState.Supply.Add(balances.Coins...) - bankGenStateBz, err := cdc.MarshalJSON(bankGenState) + bankGenState.Supply = bankGenState.Supply.Add(newSupplyCoinsCache...) + + appState[banktypes.ModuleName], err = cdc.MarshalJSON(bankGenState) if err != nil { return fmt.Errorf("failed to marshal bank genesis state: %w", err) } - appState[banktypes.ModuleName] = bankGenStateBz appStateJSON, err := json.Marshal(appState) if err != nil {