diff --git a/Makefile b/Makefile index 41fc6c7..32ab321 100644 --- a/Makefile +++ b/Makefile @@ -158,7 +158,7 @@ test-race: # TODO: Remove the -skip flag once the following tests no longer contain data races. # https://github.com/celestiaorg/celestia-app/issues/1369 @echo "--> Running tests in race mode" - @go test -mod=readonly ./... -v -race -skip "TestPrepareProposalConsistency|TestIntegrationTestSuite|TestQGBRPCQueries|TestSquareSizeIntegrationTest|TestStandardSDKIntegrationTestSuite|TestTxsimCommandFlags|TestTxsimCommandEnvVar|TestMintIntegrationTestSuite|TestQGBCLI|TestUpgrade|TestMaliciousTestNode" + @go test -mod=readonly ./... -v -race -skip "TestPrepareProposalConsistency|TestIntegrationTestSuite|TestQGBRPCQueries|TestSquareSizeIntegrationTest|TestStandardSDKIntegrationTestSuite|TestTxsimCommandFlags|TestTxsimCommandEnvVar|TestMintIntegrationTestSuite|TestQGBCLI|TestUpgrade|TestMaliciousTestNode|TestVestingModule" .PHONY: test-race ## test-bench: Run unit tests in bench mode. @@ -197,4 +197,4 @@ txsim-build-docker: adr-gen: @echo "--> Downloading ADR template" @curl -sSL https://raw.githubusercontent.com/celestiaorg/.github/main/adr-template.md > docs/architecture/adr-template.md -.PHONY: adr-gen +.PHONY: adr-gen \ No newline at end of file diff --git a/pkg/genesis/vesting_accounts.go b/pkg/genesis/vesting_accounts.go new file mode 100644 index 0000000..1d2ee8e --- /dev/null +++ b/pkg/genesis/vesting_accounts.go @@ -0,0 +1,156 @@ +package genesis + +import ( + "encoding/json" + "time" + + "github.com/celestiaorg/celestia-app/app/encoding" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func NewGenesisRegularAccount( + address string, + balances sdk.Coins, +) (account authtypes.GenesisAccount, balance banktypes.Balance, err error) { + sdkAddr, err := sdk.AccAddressFromBech32(address) + if err != nil { + return account, balance, err + } + + balance = banktypes.Balance{ + Address: address, + Coins: balances, + } + bAccount := authtypes.NewBaseAccountWithAddress(sdkAddr) + + return authtypes.GenesisAccount(bAccount), balance, nil +} + +// NewGenesisDelayedVestingAccount creates a new DelayedVestingAccount with the +// specified parameters. It returns the created account converted to genesis +// account type and the account balance. +func NewGenesisDelayedVestingAccount( + address string, + vestingBalance, + initUnlockedCoins sdk.Coins, + endTime time.Time, +) (account authtypes.GenesisAccount, balance banktypes.Balance, err error) { + sdkAddr, err := sdk.AccAddressFromBech32(address) + if err != nil { + return account, balance, err + } + + balance = banktypes.Balance{ + Address: address, + Coins: initUnlockedCoins.Add(vestingBalance...), + } + + bAccount := authtypes.NewBaseAccountWithAddress(sdkAddr) + vAccount := vestingtypes.NewDelayedVestingAccount(bAccount, vestingBalance, endTime.Unix()) + + return authtypes.GenesisAccount(vAccount), balance, nil +} + +func NewGenesisPeriodicVestingAccount( + address string, + vestingBalance, + initUnlockedCoins sdk.Coins, + startTime time.Time, + periods []vestingtypes.Period, +) (account authtypes.GenesisAccount, balance banktypes.Balance, err error) { + sdkAddr, err := sdk.AccAddressFromBech32(address) + if err != nil { + return account, balance, err + } + + balance = banktypes.Balance{ + Address: address, + Coins: initUnlockedCoins.Add(vestingBalance...), + } + + bAccount := authtypes.NewBaseAccountWithAddress(sdkAddr) + vAccount := vestingtypes.NewPeriodicVestingAccount(bAccount, vestingBalance, startTime.Unix(), periods) + + return authtypes.GenesisAccount(vAccount), balance, nil +} + +func NewGenesisContinuousVestingAccount( + address string, + vestingBalance, + initUnlockedCoins sdk.Coins, + startTime, endTime time.Time, +) (account authtypes.GenesisAccount, balance banktypes.Balance, err error) { + sdkAddr, err := sdk.AccAddressFromBech32(address) + if err != nil { + return account, balance, err + } + + balance = banktypes.Balance{ + Address: address, + Coins: initUnlockedCoins.Add(vestingBalance...), + } + + bAccount := authtypes.NewBaseAccountWithAddress(sdkAddr) + vAccount := vestingtypes.NewContinuousVestingAccount(bAccount, vestingBalance, startTime.Unix(), endTime.Unix()) + + return authtypes.GenesisAccount(vAccount), balance, nil +} + +// AddAccountsToGenesisState adds the provided accounts to the genesis state (gs) map for the auth module. +// It takes the raw genesis state (gs) and a variadic number of GenesisAccount objects (accounts) as inputs. +// Then, it updates the given genesis state and returns it. +func AddAccountsToGenesisState(encCfg encoding.Config, gs map[string]json.RawMessage, accounts ...authtypes.GenesisAccount) (map[string]json.RawMessage, error) { + var authGenState authtypes.GenesisState + err := encCfg.Codec.UnmarshalJSON(gs[authtypes.ModuleName], &authGenState) + if err != nil { + return gs, err + } + + pAccs, err := authtypes.PackAccounts(accounts) + if err != nil { + return gs, err + } + + // set the accounts in the genesis state + authGenState.Accounts = append(authGenState.Accounts, pAccs...) + gs[authtypes.ModuleName] = encCfg.Codec.MustMarshalJSON(&authGenState) + + return gs, nil +} + +// AddBalancesToGenesisState updates the genesis state by adding balances to the bank module. +func AddBalancesToGenesisState(encCfg encoding.Config, gs map[string]json.RawMessage, balances []banktypes.Balance) (map[string]json.RawMessage, error) { + var bankGenState banktypes.GenesisState + err := encCfg.Codec.UnmarshalJSON(gs[banktypes.ModuleName], &bankGenState) + if err != nil { + return gs, err + } + + bankGenState.Balances = append(bankGenState.Balances, balances...) + gs[banktypes.ModuleName] = encCfg.Codec.MustMarshalJSON(&bankGenState) + + return gs, nil +} + +// AddGenesisAccountsWithBalancesToGenesisState adds the given genesis accounts and balances to the +// provided genesis state. It returns the updated genesis state and an error if any occurred. +func AddGenesisAccountsWithBalancesToGenesisState( + encCfg encoding.Config, + gs map[string]json.RawMessage, + gAccounts []authtypes.GenesisAccount, + balances []banktypes.Balance, +) (map[string]json.RawMessage, error) { + gs, err := AddAccountsToGenesisState(encCfg, gs, gAccounts...) + if err != nil { + return gs, err + } + + gs, err = AddBalancesToGenesisState(encCfg, gs, balances) + if err != nil { + return gs, err + } + return gs, nil +} diff --git a/pkg/genesis/vesting_accounts_test.go b/pkg/genesis/vesting_accounts_test.go new file mode 100644 index 0000000..5ae447f --- /dev/null +++ b/pkg/genesis/vesting_accounts_test.go @@ -0,0 +1,499 @@ +package genesis_test + +import ( + "encoding/json" + "fmt" + "sync" + "testing" + "time" + + "github.com/celestiaorg/celestia-app/app" + "github.com/celestiaorg/celestia-app/app/encoding" + "github.com/celestiaorg/celestia-app/pkg/genesis" + "github.com/celestiaorg/celestia-app/test/util/testfactory" + "github.com/celestiaorg/celestia-app/test/util/testnode" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const ( + totalAccountsPerType = 300 + initBalanceForGasFee = 10 + vestingAmount = testfactory.BaseAccountDefaultBalance + vestingDelayPerTx = 10 // this is a safe time (in seconds) to wait for a tx to be executed while the vesting period is not over yet +) + +type accountDispenser struct { + names []string + counter int +} + +type accountType int + +const ( + RegularAccountType accountType = iota + 1 + DelayedVestingAccountType + PeriodicVestingAccountType + ContinuousVestingAccountType +) + +func (t accountType) String() string { + switch t { + case RegularAccountType: + return "regular" + case DelayedVestingAccountType: + return "delayed vesting" + case PeriodicVestingAccountType: + return "periodic vesting" + case ContinuousVestingAccountType: + return "continuous vesting" + default: + return "unknown" + } +} + +type VestingModuleTestSuite struct { + suite.Suite + + // accounts is a map from accountType to accountDispenser + accounts sync.Map + accountsMut sync.Mutex + + kr keyring.Keyring + cctx testnode.Context + ecfg encoding.Config +} + +func TestVestingModule(t *testing.T) { + if testing.Short() { + t.Skip("skipping vesting module test suite in short mode.") + } + suite.Run(t, new(VestingModuleTestSuite)) +} + +func (s *VestingModuleTestSuite) SetupSuite() { + t := s.T() + t.Log("setting up vesting module test suite") + + s.kr = testfactory.GenerateKeyring() + + s.ecfg = encoding.MakeConfig(app.ModuleEncodingRegisters...) + cfg := testnode.DefaultConfig().WithGenesisOptions(testnode.ImmediateProposals(s.ecfg.Codec)) + + cfg.GenesisOptions = []testnode.GenesisOption{ + s.initRegularAccounts(totalAccountsPerType), + s.initDelayedVestingAccounts(totalAccountsPerType), + s.initPeriodicVestingAccounts(totalAccountsPerType), + s.initContinuousVestingAccounts(totalAccountsPerType), + } + + s.cctx, _, _ = testnode.NewNetwork(s.T(), cfg) + s.cctx.Keyring = s.kr +} + +func (s *VestingModuleTestSuite) TestGenesisDelayedVestingAccountsTransferLocked() { + require.NoError(s.T(), s.cctx.WaitForNextBlock()) + + // find and test a vesting account with endTime which is + // at least 10 seconds away from now to give the tx enough time to complete + _, name, err := s.getAnUnusedDelayedVestingAccount(tmtime.Now().Unix() + vestingDelayPerTx) + require.NoError(s.T(), err) + + s.testTransferMustFail(name, vestingAmount) +} + +func (s *VestingModuleTestSuite) TestGenesisDelayedVestingAccountsTransferUnLocked() { + require.NoError(s.T(), s.cctx.WaitForNextBlock()) + + // find and test a vesting account with endTime which is already passed + vAcc, name, err := s.getAnUnusedDelayedVestingAccount(0) + require.NoError(s.T(), err) + + // Since we want a partially unlocked balance we need to wait until + // the endTime is passed if not already + for vAcc.GetVestedCoins(tmtime.Now()).IsZero() { + time.Sleep(time.Second) + } + + minExpectedSpendableBal := vAcc.GetVestedCoins(tmtime.Now()).AmountOf(app.BondDenom).Int64() + require.NotZero(s.T(), minExpectedSpendableBal) + require.NoError(s.T(), s.cctx.WaitForNextBlock()) + + // it must be able to transfer the entire vesting amount + s.testTransferMustSucceed(name, minExpectedSpendableBal) +} + +func (s *VestingModuleTestSuite) TestGenesisPeriodicVestingAccountsTransferPartiallyUnlocked() { + // Find a periodic vesting account that has some vested (unlocked) balance (its start time has already passed) + vAcc, name, err := s.getAnUnusedPeriodicVestingAccount(tmtime.Now().Unix() - 5) + require.NoError(s.T(), err) + + // Since we want a partially unlocked balance we need to wait until + // the first period has passed if not already + for vAcc.GetVestedCoins(tmtime.Now()).IsZero() { + time.Sleep(time.Second) + } + + minExpectedSpendableBal := vAcc.GetVestedCoins(tmtime.Now()).AmountOf(app.BondDenom).Int64() + require.NotZero(s.T(), minExpectedSpendableBal) + require.NoError(s.T(), s.cctx.WaitForNextBlock()) + + s.testTransferMustSucceed(name, minExpectedSpendableBal) +} + +func (s *VestingModuleTestSuite) TestGenesisPeriodicVestingAccountsTransferLocked() { + // Find a periodic vesting account that that is currently in a vesting (locked) state + // i.e. its start time yet to be reached. + vAcc, name, err := s.getAnUnusedPeriodicVestingAccount(tmtime.Now().Unix() + vestingDelayPerTx) + require.NoError(s.T(), err) + require.Zero(s.T(), vAcc.GetVestedCoins(tmtime.Now()).AmountOf(app.BondDenom).Int64()) + + s.testTransferMustFail(name, vestingAmount) +} + +func (s *VestingModuleTestSuite) TestGenesisContinuousVestingAccountsTransferLocked() { + // find a continuous vesting account with locked balance + vAcc, name, err := s.getAnUnusedContinuousVestingAccount(tmtime.Now().Unix() + vestingDelayPerTx) + require.NoError(s.T(), err) + require.Zero(s.T(), vAcc.GetVestedCoins(tmtime.Now()).AmountOf(app.BondDenom).Int64()) + + s.testTransferMustFail(name, vestingAmount) +} + +func (s *VestingModuleTestSuite) TestGenesisContinuousVestingAccountsTransferPartiallyUnlocked() { + // find a continuous vesting account with partially unlocked balance + vAcc, name, err := s.getAnUnusedContinuousVestingAccount(tmtime.Now().Unix() - 5) + require.NoError(s.T(), err) + + // Since we want a partially unlocked balance we need to wait until + // the start time just passes if not already + for vAcc.GetVestedCoins(tmtime.Now()).IsZero() { + time.Sleep(time.Second) + } + + minExpectedSpendableBal := vAcc.GetVestedCoins(tmtime.Now()).AmountOf(app.BondDenom).Int64() + require.NotZero(s.T(), minExpectedSpendableBal) + require.NoError(s.T(), s.cctx.WaitForNextBlock()) + + s.testTransferMustSucceed(name, minExpectedSpendableBal) +} + +// testTransferMustFail tests the transfer of an amount (which must be locked to fail) +// from a vesting account to another account. It asserts that the result code of the +// transaction is equal to 5, indicating an InsufficientFunds error. +func (s *VestingModuleTestSuite) testTransferMustFail(name string, amount int64) { + txResultCode, err := s.submitTransferTx(name, amount) + assert.NoError(s.T(), err) + assert.EqualValues(s.T(), sdkerrors.ErrInsufficientFunds.ABCICode(), txResultCode, "the transfer TX must fail") +} + +// testTransferMustSucceed tests the transfer of a certain amount of funds from one account +// to another. It asserts that the result code of the transaction is equal to 0, indicating +// a success. +func (s *VestingModuleTestSuite) testTransferMustSucceed(name string, amount int64) { + txResultCode, err := s.submitTransferTx(name, amount) + assert.NoError(s.T(), err) + assert.EqualValues(s.T(), abci.CodeTypeOK, txResultCode, "the transfer TX must succeed") +} + +// submitTransferTx submits a transfer transaction to a random account and returns the tx result code +func (s *VestingModuleTestSuite) submitTransferTx(name string, amount int64) (txResultCode uint32, err error) { + randomAcc, err := s.unusedAccount(RegularAccountType) + if err != nil { + return 0, err + } + + msgSend := banktypes.NewMsgSend( + getAddress(name, s.cctx.Keyring), + getAddress(randomAcc, s.cctx.Keyring), + sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(amount))), + ) + resTx, err := testnode.SignAndBroadcastTx(s.ecfg, s.cctx.Context, name, []sdk.Msg{msgSend}...) + if err != nil { + return 0, err + } + + resQ, err := s.cctx.WaitForTx(resTx.TxHash, 10) + if err != nil { + return 0, err + } + return resQ.TxResult.Code, nil +} + +// initRegularAccounts initializes regular accounts for the VestingModuleTestSuite. +// It generates the specified number of account names and stores them in the accounts map +// of the VestingModuleTestSuite with RegularAccountType as the key. It also generates base accounts +// and their default balances. The generated accounts and balances are added to the provided genesis +// state. The genesis state modifier function is returned. +func (s *VestingModuleTestSuite) initRegularAccounts(count int) testnode.GenesisOption { + names := testfactory.GenerateAccounts(count) + s.accounts.Store(RegularAccountType, accountDispenser{names: names}) + + gAccounts := authtypes.GenesisAccounts{} + balances := []banktypes.Balance{} + + for i := range names { + bAccount, defaultBalance := testfactory.NewBaseAccount(s.kr, names[i]) + gAccount, balance, err := genesis.NewGenesisRegularAccount( + bAccount.GetAddress().String(), + defaultBalance, + ) + require.NoError(s.T(), err) + + gAccounts = append(gAccounts, gAccount) + balances = append(balances, balance) + } + + return func(gs map[string]json.RawMessage) map[string]json.RawMessage { + gs, err := genesis.AddGenesisAccountsWithBalancesToGenesisState(s.ecfg, gs, gAccounts, balances) + assert.NoError(s.T(), err) + return gs + } +} + +// initDelayedVestingAccounts initializes delayed vesting accounts for the VestingModuleTestSuite. +// It generates the specified number of account names and stores them in the accounts map with +// DelayedVestingAccountType as the key. +func (s *VestingModuleTestSuite) initDelayedVestingAccounts(count int) testnode.GenesisOption { + initCoinsForGasFee := sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(initBalanceForGasFee))) + + names := testfactory.GenerateAccounts(count) + s.accounts.Store(DelayedVestingAccountType, accountDispenser{names: names}) + + gAccounts := authtypes.GenesisAccounts{} + balances := []banktypes.Balance{} + + endTime := tmtime.Now().Add(-2 * time.Second) + for i := range names { + bAccount, defaultBalance := testfactory.NewBaseAccount(s.kr, names[i]) + gAccount, balance, err := genesis.NewGenesisDelayedVestingAccount( + bAccount.GetAddress().String(), + defaultBalance, + initCoinsForGasFee, + endTime) + require.NoError(s.T(), err) + + gAccounts = append(gAccounts, gAccount) + balances = append(balances, balance) + + // the endTime is increased for each account to be able to test various scenarios + endTime = endTime.Add(5 * time.Second) + } + + return func(gs map[string]json.RawMessage) map[string]json.RawMessage { + gs, err := genesis.AddGenesisAccountsWithBalancesToGenesisState(s.ecfg, gs, gAccounts, balances) + require.NoError(s.T(), err) + return gs + } +} + +// initPeriodicVestingAccounts function initializes periodic vesting accounts for testing purposes. +// It takes the count of accounts as input and returns a GenesisOption. It generates account names, +// base accounts, and balances. It defines vesting periods and creates vesting accounts based on the +// generated data. The startTime of each account increases progressively to ensure some accounts have +// locked balances, catering to the testing requirements. +func (s *VestingModuleTestSuite) initPeriodicVestingAccounts(count int) testnode.GenesisOption { + initCoinsForGasFee := sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(initBalanceForGasFee))) + + names := testfactory.GenerateAccounts(count) + s.accounts.Store(PeriodicVestingAccountType, accountDispenser{names: names}) + + allocationPerPeriod := vestingAmount / 4 + periods := vestingtypes.Periods{ + vestingtypes.Period{Length: int64(6), Amount: sdk.Coins{sdk.NewInt64Coin(app.BondDenom, allocationPerPeriod)}}, + vestingtypes.Period{Length: int64(6), Amount: sdk.Coins{sdk.NewInt64Coin(app.BondDenom, allocationPerPeriod)}}, + vestingtypes.Period{Length: int64(6), Amount: sdk.Coins{sdk.NewInt64Coin(app.BondDenom, allocationPerPeriod)}}, + vestingtypes.Period{Length: int64(6), Amount: sdk.Coins{sdk.NewInt64Coin(app.BondDenom, allocationPerPeriod)}}, + } + + gAccounts := authtypes.GenesisAccounts{} + balances := []banktypes.Balance{} + + startTime := tmtime.Now() + for i := range names { + bAccount, defaultBalance := testfactory.NewBaseAccount(s.kr, names[i]) + gAccount, balance, err := genesis.NewGenesisPeriodicVestingAccount( + bAccount.GetAddress().String(), + defaultBalance, + initCoinsForGasFee, + startTime, + periods, + ) + require.NoError(s.T(), err) + + gAccounts = append(gAccounts, gAccount) + balances = append(balances, balance) + + // the startTime is increased for each account to be able to test various scenarios + startTime = startTime.Add(5 * time.Second) + } + + return func(gs map[string]json.RawMessage) map[string]json.RawMessage { + gs, err := genesis.AddGenesisAccountsWithBalancesToGenesisState(s.ecfg, gs, gAccounts, balances) + assert.NoError(s.T(), err) + return gs + } +} + +// initContinuousVestingAccounts function initializes continuous vesting accounts for testing purposes. +// It takes the count of accounts as input and returns a GenesisOption. It generates account names, +// base accounts, and balances. It defines start & end times to creates vesting accounts. The start & endTime +// of each account increases progressively to ensure some accounts have locked balances, catering to the +// testing requirements. +func (s *VestingModuleTestSuite) initContinuousVestingAccounts(count int) testnode.GenesisOption { + initCoinsForGasFee := sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(initBalanceForGasFee))) + + names := testfactory.GenerateAccounts(count) + s.accounts.Store(ContinuousVestingAccountType, accountDispenser{names: names}) + + gAccounts := authtypes.GenesisAccounts{} + balances := []banktypes.Balance{} + startTime := tmtime.Now() + + for i := range names { + endTime := startTime.Add(20 * time.Second) + + bAccount, defaultBalance := testfactory.NewBaseAccount(s.kr, names[i]) + gAccount, balance, err := genesis.NewGenesisContinuousVestingAccount( + bAccount.GetAddress().String(), + defaultBalance, + initCoinsForGasFee, + startTime, + endTime, + ) + require.NoError(s.T(), err) + + gAccounts = append(gAccounts, gAccount) + balances = append(balances, balance) + + // the startTime is increased for each account to be able to test various scenarios + startTime = startTime.Add(5 * time.Second) + } + + return func(gs map[string]json.RawMessage) map[string]json.RawMessage { + gs, err := genesis.AddGenesisAccountsWithBalancesToGenesisState(s.ecfg, gs, gAccounts, balances) + assert.NoError(s.T(), err) + return gs + } +} + +// unusedAccount returns an unused account name of the specified account type +// for the VestingModuleTestSuite. If the account type is not found, it returns +// an error. +func (s *VestingModuleTestSuite) unusedAccount(accType accountType) (string, error) { + s.accountsMut.Lock() + defer s.accountsMut.Unlock() + + accountsAny, found := s.accounts.Load(accType) + if !found { + return "", fmt.Errorf("account type `%s` not found", accType.String()) + } + + accounts := accountsAny.(accountDispenser) + if accounts.counter >= len(accounts.names) { + return "", fmt.Errorf("out of unused accounts for type `%s`", accType.String()) + } + + name := accounts.names[accounts.counter] + accounts.counter++ + s.accounts.Store(accType, accounts) + + return name, nil +} + +// getAnUnusedContinuousVestingAccount returns an unused continuous vesting account and its name. +// +// It takes a minimum start-time as input and finds an unused account whose start time is greater than the input. +func (s *VestingModuleTestSuite) getAnUnusedContinuousVestingAccount(minStartTime int64) (vAcc vestingtypes.ContinuousVestingAccount, name string, err error) { + for { + name, err = s.unusedAccount(ContinuousVestingAccountType) + if err != nil { + return vAcc, name, err + } + address := getAddress(name, s.cctx.Keyring).String() + resAccBytes, err := testfactory.GetRawAccountInfo(s.cctx.GRPCClient, address) + if err != nil { + return vestingtypes.ContinuousVestingAccount{}, "", err + } + + err = vAcc.Unmarshal(resAccBytes) + if err != nil { + return vestingtypes.ContinuousVestingAccount{}, "", err + } + if vAcc.StartTime > minStartTime { + return vAcc, name, nil + } + } +} + +// getAnUnusedPeriodicVestingAccount returns an unused periodic vesting account and its name. +// +// It takes a minimum start-time as input and finds an unused account whose start time is greater than the input. +func (s *VestingModuleTestSuite) getAnUnusedPeriodicVestingAccount(minStartTime int64) (vAcc vestingtypes.PeriodicVestingAccount, name string, err error) { + for { + name, err = s.unusedAccount(PeriodicVestingAccountType) + if err != nil { + return vAcc, name, err + } + address := getAddress(name, s.cctx.Keyring).String() + resAccBytes, err := testfactory.GetRawAccountInfo(s.cctx.GRPCClient, address) + if err != nil { + return vestingtypes.PeriodicVestingAccount{}, "", err + } + + err = vAcc.Unmarshal(resAccBytes) + if err != nil { + return vestingtypes.PeriodicVestingAccount{}, "", err + } + if vAcc.StartTime > minStartTime { + return vAcc, name, nil + } + } +} + +// getAnUnusedDelayedVestingAccount returns the name of an unused delayed vesting account. +// +// It takes a minimum end-time as input and finds an unused account whose end time is greater than the input. +func (s *VestingModuleTestSuite) getAnUnusedDelayedVestingAccount(minEndTime int64) (vAcc vestingtypes.DelayedVestingAccount, name string, err error) { + for { + name, err = s.unusedAccount(DelayedVestingAccountType) + if err != nil { + return vestingtypes.DelayedVestingAccount{}, "", err + } + + address := getAddress(name, s.cctx.Keyring).String() + resAccBytes, err := testfactory.GetRawAccountInfo(s.cctx.GRPCClient, address) + if err != nil { + return vestingtypes.DelayedVestingAccount{}, "", err + } + + err = vAcc.Unmarshal(resAccBytes) + if err != nil { + return vestingtypes.DelayedVestingAccount{}, "", err + } + if vAcc.EndTime > minEndTime { + return vAcc, name, nil + } + } +} + +func getAddress(account string, kr keyring.Keyring) sdk.AccAddress { + rec, err := kr.Key(account) + if err != nil { + panic(err) + } + addr, err := rec.GetAddress() + if err != nil { + panic(err) + } + return addr +} diff --git a/test/util/testfactory/utils.go b/test/util/testfactory/utils.go index 7b8eff4..dc7d12d 100644 --- a/test/util/testfactory/utils.go +++ b/test/util/testfactory/utils.go @@ -11,15 +11,18 @@ import ( 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" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" tmrand "github.com/tendermint/tendermint/libs/rand" rpctypes "github.com/tendermint/tendermint/rpc/core/types" + "google.golang.org/grpc" ) const ( // nolint:lll - TestAccName = "test-account" - TestAccMnemo = `ramp soldier connect gadget domain mutual staff unusual first midnight iron good deputy wage vehicle mutual spike unlock rocket delay hundred script tumble choose` - bondDenom = "utia" + TestAccName = "test-account" + TestAccMnemo = `ramp soldier connect gadget domain mutual staff unusual first midnight iron good deputy wage vehicle mutual spike unlock rocket delay hundred script tumble choose` + bondDenom = "utia" + BaseAccountDefaultBalance = int64(10000) ) func QueryWithoutProof(clientCtx client.Context, hashHexStr string) (*rpctypes.ResultTx, error) { @@ -102,3 +105,70 @@ func GenerateAccounts(count int) []string { } return accs } + +// NewBaseAccount creates a new base account. +// If an empty string is passed as a name, a random one will be generated and used. +// +// It takes a keyring and a name as its parameters. +// It returns a BaseAccount and a slice of sdk Coins with the default bond denom. +func NewBaseAccount(kr keyring.Keyring, name string) (*authtypes.BaseAccount, sdk.Coins) { + if name == "" { + name = tmrand.Str(6) + } + rec, _, err := kr.NewMnemonic(name, keyring.English, "", "", hd.Secp256k1) + if err != nil { + panic(err) + } + addr, err := rec.GetAddress() + if err != nil { + panic(err) + } + origCoins := sdk.Coins{sdk.NewInt64Coin(bondDenom, BaseAccountDefaultBalance)} + bacc := authtypes.NewBaseAccountWithAddress(addr) + return bacc, origCoins +} + +func GetValidators(grpcConn *grpc.ClientConn) (stakingtypes.Validators, error) { + scli := stakingtypes.NewQueryClient(grpcConn) + vres, err := scli.Validators(context.Background(), &stakingtypes.QueryValidatorsRequest{}) + if err != nil { + return stakingtypes.Validators{}, err + } + return vres.Validators, nil +} + +func GetAccountDelegations(grpcConn *grpc.ClientConn, address string) (stakingtypes.DelegationResponses, error) { + cli := stakingtypes.NewQueryClient(grpcConn) + res, err := cli.DelegatorDelegations(context.Background(), + &stakingtypes.QueryDelegatorDelegationsRequest{DelegatorAddr: address}) + if err != nil { + return nil, err + } + + return res.DelegationResponses, nil +} + +func GetAccountSpendableBalance(grpcConn *grpc.ClientConn, address string) (balances sdk.Coins, err error) { + cli := banktypes.NewQueryClient(grpcConn) + res, err := cli.SpendableBalances( + context.Background(), + &banktypes.QuerySpendableBalancesRequest{ + Address: address, + }, + ) + if err != nil { + return nil, err + } + return res.GetBalances(), nil +} + +func GetRawAccountInfo(grpcConn *grpc.ClientConn, address string) ([]byte, error) { + cli := authtypes.NewQueryClient(grpcConn) + res, err := cli.Account(context.Background(), &authtypes.QueryAccountRequest{ + Address: address, + }) + if err != nil { + return nil, err + } + return res.Account.Value, nil +} diff --git a/test/util/testnode/node_interaction_api.go b/test/util/testnode/node_interaction_api.go index 00feabf..4155401 100644 --- a/test/util/testnode/node_interaction_api.go +++ b/test/util/testnode/node_interaction_api.go @@ -325,3 +325,19 @@ func (c *Context) HeightForTimestamp(timestamp time.Time) (int64, error) { } return 0, fmt.Errorf("could not find block with timestamp after %v", timestamp) } + +// LatestBlock retrieves the latest block from the context. +// +// It returns a pointer to the latest block and an error if any. +func (c *Context) LatestBlock() (*coretypes.Block, error) { + height, err := c.LatestHeight() + if err != nil { + return nil, err + } + + result, err := c.Client.Block(context.Background(), &height) + if err != nil { + return nil, err + } + return result.Block, nil +} diff --git a/test/util/testnode/sign.go b/test/util/testnode/sign.go index da3a994..98e8f49 100644 --- a/test/util/testnode/sign.go +++ b/test/util/testnode/sign.go @@ -20,7 +20,11 @@ func SignAndBroadcastTx(encCfg encoding.Config, c client.Context, account string sdk.NewCoin(app.BondDenom, sdk.NewInt(1)), )), } + return SignAndBroadcastTxWithBuilderOption(opts, encCfg, c, account, msg...) +} +// SignAndBroadcastTxWithBuilderOption signs and broadcasts a transaction with the given options. +func SignAndBroadcastTxWithBuilderOption(opts []types.TxBuilderOption, encCfg encoding.Config, c client.Context, account string, msg ...sdk.Msg) (res *sdk.TxResponse, err error) { // use the key for accounts[i] to create a signer used for a single PFB signer := types.NewKeyringSigner(c.Keyring, account, c.ChainID)