From 4b4bafbc61a8d2ffb46fa1a4ca28eb35435b0d70 Mon Sep 17 00:00:00 2001 From: Benjamin DENEUX Date: Fri, 17 May 2024 15:54:38 +0200 Subject: [PATCH] fix(vesting): reintroduce the add-genesis-account custom command to include cliff vesting account --- cmd/axoned/cmd/genaccount.go | 105 +++++++++++++++++++++ cmd/axoned/cmd/genaccount_test.go | 116 +++++++++++++++++++++++ cmd/axoned/cmd/root.go | 9 ++ x/vesting/types/genaccounts.go | 149 ++++++++++++++++++++++++++++++ 4 files changed, 379 insertions(+) create mode 100644 cmd/axoned/cmd/genaccount.go create mode 100644 cmd/axoned/cmd/genaccount_test.go create mode 100644 x/vesting/types/genaccounts.go diff --git a/cmd/axoned/cmd/genaccount.go b/cmd/axoned/cmd/genaccount.go new file mode 100644 index 00000000..a1c7c6ee --- /dev/null +++ b/cmd/axoned/cmd/genaccount.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "bufio" + "fmt" + + "github.com/spf13/cobra" + + address "cosmossdk.io/core/address" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + + vestingtypes "github.com/axone-protocol/axoned/v7/x/vesting/types" +) + +const ( + flagVestingStart = "vesting-start-time" + flagVestingCliff = "vesting-cliff-time" + flagVestingEnd = "vesting-end-time" + flagVestingAmt = "vesting-amount" + flagAppendMode = "append" + flagModuleName = "module-name" +) + +// AddGenesisAccountCmd returns 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. +// +//nolint:funlen,nestif +func AddGenesisAccountCmd(defaultNodeHome string, addressCodec address.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-genesis-account [address_or_key_name] [coin][,[coin]]", + Short: "Add a genesis account to genesis.json", + Long: `Add a genesis account to genesis.json. The provided account must specify +the account address or key name and a list of initial coins. If a key name is given, +the address will be looked up in the local Keybase. The list of initial tokens must +contain valid denominations. Accounts may optionally be supplied with vesting parameters. +`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx := client.GetClientContextFromCmd(cmd) + serverCtx := server.GetServerContextFromCmd(cmd) + config := serverCtx.Config + + config.SetRoot(clientCtx.HomeDir) + + var kr keyring.Keyring + addr, err := addressCodec.StringToBytes(args[0]) + if err != nil { + inBuf := bufio.NewReader(cmd.InOrStdin()) + keyringBackend, _ := cmd.Flags().GetString(flags.FlagKeyringBackend) + + if keyringBackend != "" && clientCtx.Keyring == nil { + var err error + kr, err = keyring.New(sdk.KeyringServiceName(), keyringBackend, clientCtx.HomeDir, inBuf, clientCtx.Codec) + if err != nil { + return err + } + } else { + kr = clientCtx.Keyring + } + + k, err := kr.Key(args[0]) + if err != nil { + return fmt.Errorf("failed to get address from Keyring: %w", err) + } + + addr, err = k.GetAddress() + if err != nil { + return err + } + } + + appendflag, _ := cmd.Flags().GetBool(flagAppendMode) + vestingStart, _ := cmd.Flags().GetInt64(flagVestingStart) + vestingCliff, _ := cmd.Flags().GetInt64(flagVestingCliff) + vestingEnd, _ := cmd.Flags().GetInt64(flagVestingEnd) + vestingAmtStr, _ := cmd.Flags().GetString(flagVestingAmt) + moduleNameStr, _ := cmd.Flags().GetString(flagModuleName) + + return vestingtypes.AddGenesisAccount(clientCtx.Codec, + addr, + appendflag, + config.GenesisFile(), + args[1], + vestingAmtStr, vestingStart, vestingCliff, vestingEnd, + moduleNameStr) + }, + } + + cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory") + cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|kwallet|pass|test)") + cmd.Flags().String(flagVestingAmt, "", "amount of coins for vesting accounts") + cmd.Flags().Int64(flagVestingStart, 0, "schedule start time (unix epoch) for vesting accounts") + cmd.Flags().Int64(flagVestingCliff, 0, "schedule cliff time (unix epoch) for vesting accounts") + cmd.Flags().Int64(flagVestingEnd, 0, "schedule end time (unix epoch) for vesting accounts") + cmd.Flags().Bool(flagAppendMode, false, "append the coins to an account already in the genesis.json file") + cmd.Flags().String(flagModuleName, "", "module account name") + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/cmd/axoned/cmd/genaccount_test.go b/cmd/axoned/cmd/genaccount_test.go new file mode 100644 index 00000000..45d33ba2 --- /dev/null +++ b/cmd/axoned/cmd/genaccount_test.go @@ -0,0 +1,116 @@ +package cmd_test + +import ( + "context" + "fmt" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "cosmossdk.io/log" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + addresscodec "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + 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" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +var testMbm = module.NewBasicManager( + staking.AppModuleBasic{}, + genutil.AppModuleBasic{}, +) + +func TestAddGenesisAccountCmd(t *testing.T) { + _, _, addr1 := testdata.KeyTestPubAddr() + tests := []struct { + name string + addr string + denom string + withKeyring bool + expectErr bool + }{ + { + name: "invalid address", + addr: "", + denom: "1000atom", + withKeyring: false, + expectErr: true, + }, + { + name: "valid address", + addr: addr1.String(), + denom: "1000atom", + withKeyring: false, + expectErr: false, + }, + { + name: "multiple denoms", + addr: addr1.String(), + denom: "1000atom, 2000stake", + withKeyring: false, + expectErr: false, + }, + { + name: "with keyring", + addr: "ser", + denom: "1000atom", + withKeyring: true, + expectErr: false, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + home := t.TempDir() + logger := log.NewNopLogger() + cfg, err := genutiltest.CreateDefaultCometConfig(home) + require.NoError(t, err) + + appCodec := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}).Codec + err = genutiltest.ExecInitCmd(testMbm, home, appCodec) + require.NoError(t, err) + + serverCtx := server.NewContext(viper.New(), cfg, logger) + clientCtx := client.Context{}.WithCodec(appCodec).WithHomeDir(home) + + if tc.withKeyring { + path := hd.CreateHDPath(118, 0, 0).String() + kr, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendMemory, home, nil, appCodec) + require.NoError(t, err) + _, _, err = kr.NewMnemonic(tc.addr, keyring.English, path, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + require.NoError(t, err) + clientCtx = clientCtx.WithKeyring(kr) + } + + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx) + + cmd := genutilcli.AddGenesisAccountCmd(home, addresscodec.NewBech32Codec("cosmos")) + cmd.SetArgs([]string{ + tc.addr, + tc.denom, + fmt.Sprintf("--%s=home", flags.FlagHome), + }) + + if tc.expectErr { + require.Error(t, cmd.ExecuteContext(ctx)) + } else { + require.NoError(t, cmd.ExecuteContext(ctx)) + } + }) + } +} diff --git a/cmd/axoned/cmd/root.go b/cmd/axoned/cmd/root.go index 56687817..20f5ffc0 100644 --- a/cmd/axoned/cmd/root.go +++ b/cmd/axoned/cmd/root.go @@ -196,9 +196,18 @@ func initRootCmd( func genesisCommand(txConfig client.TxConfig, basicManager module.BasicManager, cmds ...*cobra.Command) *cobra.Command { cmd := genutilcli.Commands(txConfig, basicManager, app.DefaultNodeHome) + // Remove default `add-genesis-account` command and add our custom (integrating the cliff), + for _, command := range cmd.Commands() { + if command.Name() == "add-genesis-account" { + cmd.RemoveCommand(command) + } + } + cmd.AddCommand(AddGenesisAccountCmd(app.DefaultNodeHome, txConfig.SigningContext().AddressCodec())) + for _, subCmd := range cmds { cmd.AddCommand(subCmd) } + return cmd } diff --git a/x/vesting/types/genaccounts.go b/x/vesting/types/genaccounts.go new file mode 100644 index 00000000..d67a4705 --- /dev/null +++ b/x/vesting/types/genaccounts.go @@ -0,0 +1,149 @@ +package types + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + 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. +// +//nolint:funlen,cyclop,nestif +func AddGenesisAccount( + cdc codec.Codec, + accAddr sdk.AccAddress, + appendAcct bool, + genesisFileURL, amountStr, vestingAmtStr string, + vestingStart, vestingCliff, vestingEnd int64, + moduleName string, +) error { + coins, err := sdk.ParseCoinsNormalized(amountStr) + if err != nil { + return fmt.Errorf("failed to parse coins: %w", err) + } + + vestingAmt, err := sdk.ParseCoinsNormalized(vestingAmtStr) + if err != nil { + return fmt.Errorf("failed to parse vesting amount: %w", err) + } + + // create concrete account type based on input parameters + var genAccount authtypes.GenesisAccount + + balances := banktypes.Balance{Address: accAddr.String(), Coins: coins.Sort()} + baseAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0) + + switch { + case !vestingAmt.IsZero(): + baseVestingAccount, err := NewBaseVestingAccount(baseAccount, vestingAmt.Sort(), vestingEnd) + if err != nil { + return fmt.Errorf("failed to create base vesting 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") + } + + switch { + case vestingStart != 0 && vestingCliff != 0: + genAccount = NewCliffVestingAccountRaw(baseVestingAccount, vestingStart, vestingCliff) + + case vestingStart != 0 && vestingEnd != 0: + genAccount = NewContinuousVestingAccountRaw(baseVestingAccount, vestingStart) + + case vestingEnd != 0: + genAccount = NewDelayedVestingAccountRaw(baseVestingAccount) + + default: + return errors.New("invalid vesting parameters; must supply start and end time or end time") + } + case moduleName != "": + genAccount = authtypes.NewEmptyModuleAccount(moduleName, authtypes.Burner, authtypes.Minter) + default: + genAccount = baseAccount + } + + if err := genAccount.Validate(); err != nil { + return fmt.Errorf("failed to validate new genesis account: %w", err) + } + + appState, appGenesis, err := genutiltypes.GenesisStateFromGenFile(genesisFileURL) + if err != nil { + return fmt.Errorf("failed to unmarshal genesis state: %w", err) + } + + authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState) + + accs, err := authtypes.UnpackAccounts(authGenState.Accounts) + if err != nil { + return fmt.Errorf("failed to get accounts from any: %w", err) + } + + 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) + } + + genesisB := banktypes.GetGenesisStateFromAppState(cdc, appState) + for idx, acc := range genesisB.Balances { + if acc.Address != accAddr.String() { + continue + } + + updatedCoins := acc.Coins.Add(coins...) + bankGenState.Balances[idx] = banktypes.Balance{Address: accAddr.String(), 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) + + genAccs, err := authtypes.PackAccounts(accs) + if err != nil { + return fmt.Errorf("failed to convert accounts into any's: %w", err) + } + 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 + + bankGenState.Balances = append(bankGenState.Balances, balances) + } + + bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances) + + bankGenState.Supply = bankGenState.Supply.Add(balances.Coins...) + + bankGenStateBz, 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 { + return fmt.Errorf("failed to marshal application genesis state: %w", err) + } + + appGenesis.AppState = appStateJSON + return genutil.ExportGenesisFile(appGenesis, genesisFileURL) +}