diff --git a/tests/e2e/address.go b/tests/e2e/address.go new file mode 100644 index 00000000..14d9eac6 --- /dev/null +++ b/tests/e2e/address.go @@ -0,0 +1,33 @@ +package e2e + +import ( + "fmt" + "math/rand" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +// HDPath generates an HD path based on the wallet index +func HDPath(index int) string { + return fmt.Sprintf("m/44'/118'/0'/0/%d", index) +} + +// PubKey returns a sample account PubKey +func PubKey() crypto.PubKey { + seed := []byte(strconv.Itoa(rand.Int())) + return ed25519.GenPrivKeyFromSecret(seed).PubKey() +} + +// AccAddress returns a sample account address +func AccAddress() sdk.AccAddress { + addr := PubKey().Address() + return sdk.AccAddress(addr) +} + +// Address returns a sample string account address +func Address() string { + return AccAddress().String() +} diff --git a/tests/e2e/chain.go b/tests/e2e/chain.go index bae425db..eea8e46b 100644 --- a/tests/e2e/chain.go +++ b/tests/e2e/chain.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" + authvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" gaia "github.com/cosmos/gaia/v8/app" "github.com/cosmos/gaia/v8/app/params" @@ -38,6 +39,7 @@ func init() { &ed25519.PubKey{}, ) + authvesting.RegisterInterfaces(encodingConfig.InterfaceRegistry) cdc = encodingConfig.Codec } @@ -46,8 +48,10 @@ type chain struct { dataDir string id string validators []*validator + accounts []*account // initial accounts in genesis - genesisAccounts []*account + genesisAccounts []*account + genesisVestingAccounts map[string]sdk.AccAddress } func newChain() (*chain, error) { diff --git a/tests/e2e/e2e_exec_test.go b/tests/e2e/e2e_exec_test.go index 7bb88089..d71ceda6 100644 --- a/tests/e2e/e2e_exec_test.go +++ b/tests/e2e/e2e_exec_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" "strconv" "strings" "time" @@ -21,6 +22,93 @@ import ( "github.com/ory/dockertest/v3/docker" ) +const ( + flagFrom = "from" + flagHome = "home" + flagFees = "fees" + flagGas = "gas" + flagOutput = "output" + flagChainID = "chain-id" + flagBroadcastMode = "broadcast-mode" + flagKeyringBackend = "keyring-backend" +) + +type flagOption func(map[string]interface{}) + +// withKeyValue add a new flag to command +func withKeyValue(key string, value interface{}) flagOption { + return func(o map[string]interface{}) { + o[key] = value + } +} + +func applyOptions(chainID string, options []flagOption) map[string]interface{} { + opts := map[string]interface{}{ + flagKeyringBackend: "test", + flagOutput: "json", + flagGas: "auto", + flagFrom: "alice", + flagBroadcastMode: "sync", + flagChainID: chainID, + flagHome: gaiaHomePath, + flagFees: fees.String(), + } + for _, apply := range options { + apply(opts) + } + return opts +} + +func (s *IntegrationTestSuite) execVestingTx( + c *chain, + method string, + args []string, + opt ...flagOption, +) { + opts := applyOptions(c.id, opt) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + s.T().Logf("%s - Executing gaiad %s with %v", c.id, method, args) + gaiaCommand := []string{ + gaiadBinary, + "tx", + "vesting", + method, + "-y", + } + gaiaCommand = append(gaiaCommand, args...) + + for flag, value := range opts { + gaiaCommand = append(gaiaCommand, fmt.Sprintf("--%s=%v", flag, value)) + } + + s.executeGaiaTxCommand(ctx, c, gaiaCommand, 0, s.defaultExecValidation(c, 0)) + s.T().Logf("successfully %s with %v", method, args) +} + +func (s *IntegrationTestSuite) execCreatePermanentLockedAccount( + c *chain, + address, + amount string, + opt ...flagOption, +) { + s.T().Logf("Executing gaiad create a permanent locked vesting account %s", c.id) + s.execVestingTx(c, "create-permanent-locked-account", []string{address, amount}, opt...) + s.T().Logf("successfully created permanent locked vesting account %s with %s", address, amount) +} + +func (s *IntegrationTestSuite) execCreatePeriodicVestingAccount( + c *chain, + address string, + opt ...flagOption, +) { + jsonPath := filepath.Join(gaiaHomePath, vestingPeriodFilePath) + s.T().Logf("Executing gaiad create periodic vesting account %s", c.id) + s.execVestingTx(c, "create-periodic-vesting-account", []string{address, jsonPath}, opt...) + s.T().Logf("successfully created periodic vesting account %s with %s", address, jsonPath) +} + func (s *IntegrationTestSuite) execBankSend(c *chain, valIdx int, from, to, amt, fees string, expectErr bool) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go index f9b1ab7d..efb691fd 100644 --- a/tests/e2e/e2e_setup_test.go +++ b/tests/e2e/e2e_setup_test.go @@ -15,11 +15,14 @@ import ( "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/server" srvconfig "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" @@ -36,6 +39,7 @@ import ( "github.com/stretchr/testify/suite" tmconfig "github.com/tendermint/tendermint/config" tmjson "github.com/tendermint/tendermint/libs/json" + "github.com/tendermint/tendermint/libs/rand" rpchttp "github.com/tendermint/tendermint/rpc/client/http" ) @@ -50,34 +54,51 @@ const ( minGasPrice = "0.00001" // the test globalfee in genesis is the same as minGasPrice // global fee lower/higher than min_gas_price - initialGlobalFeeAmt = "0.00001" - lowGlobalFeesAmt = "0.000001" - highGlobalFeeAmt = "0.0001" - gas = 200000 - govSendMsgRecipientAddress = "cosmos1pkueemdeps77dwrqma03pwqk93nw39nuhccz02" - govProposalBlockBuffer = 35 - relayerAccountIndex = 0 - icaOwnerAccountIndex = 1 + initialGlobalFeeAmt = "0.00001" + lowGlobalFeesAmt = "0.000001" + highGlobalFeeAmt = "0.0001" + gas = 200000 + govProposalBlockBuffer = 35 + relayerAccountIndex = 0 + icaOwnerAccountIndex = 1 ) var ( - gaiaConfigPath = filepath.Join(gaiaHomePath, "config") - stakingAmount = math.NewInt(100000000000) - stakingAmountCoin = sdk.NewCoin(uatomDenom, stakingAmount) - tokenAmount = sdk.NewCoin(uatomDenom, math.NewInt(3300000000)) // 3,300uatom - fees = sdk.NewCoin(uatomDenom, math.NewInt(330000)) // 0.33uatom - depositAmount = sdk.NewCoin(uatomDenom, math.NewInt(10000000)) // 10uatom - distModuleAddress = authtypes.NewModuleAddress(distrtypes.ModuleName).String() - govModuleAddress = authtypes.NewModuleAddress(gov.ModuleName).String() - proposalCounter = 0 - sendGovAmount = sdk.NewInt64Coin(uatomDenom, 10) - fundGovAmount = sdk.NewInt64Coin(uatomDenom, 1000) - proposalSendMsg = &govtypes.MsgSubmitProposal{ + gaiaConfigPath = filepath.Join(gaiaHomePath, "config") + stakingAmount = math.NewInt(100000000000) + stakingAmountCoin = sdk.NewCoin(uatomDenom, stakingAmount) + tokenAmount = sdk.NewCoin(uatomDenom, math.NewInt(3300000000)) // 3,300uatom + fees = sdk.NewCoin(uatomDenom, math.NewInt(330000)) // 0.33uatom + depositAmount = sdk.NewCoin(uatomDenom, math.NewInt(10000000)) // 10uatom + distModuleAddress = authtypes.NewModuleAddress(distrtypes.ModuleName).String() + govModuleAddress = authtypes.NewModuleAddress(gov.ModuleName).String() + proposalCounter = 0 + govSendMsgRecipientAddress = Address() + sendGovAmount = sdk.NewInt64Coin(uatomDenom, 10) + fundGovAmount = sdk.NewInt64Coin(uatomDenom, 1000) + proposalSendMsg = &govtypes.MsgSubmitProposal{ InitialDeposit: sdk.Coins{depositAmount}, Metadata: b64.StdEncoding.EncodeToString([]byte("Testing 1, 2, 3!")), } ) +type UpgradePlan struct { + Name string `json:"name"` + Height int `json:"height"` + Info string `json:"info"` +} + +type SoftwareUpgrade struct { + Type string `json:"@type"` + Authority string `json:"authority"` + Plan UpgradePlan `json:"plan"` +} + +type CancelSoftwareUpgrade struct { + Type string `json:"@type"` + Authority string `json:"authority"` +} + type IntegrationTestSuite struct { suite.Suite @@ -119,6 +140,9 @@ func (s *IntegrationTestSuite) SetupSuite() { s.valResources = make(map[string][]*dockertest.Resource) + vestingMnemonic, err := createMnemonic() + s.Require().NoError(err) + // The boostrapping phase is as follows: // // 1. Initialize Gaia validator nodes. @@ -128,13 +152,13 @@ func (s *IntegrationTestSuite) SetupSuite() { s.T().Logf("starting e2e infrastructure for chain A; chain-id: %s; datadir: %s", s.chainA.id, s.chainA.dataDir) s.initNodes(s.chainA) - s.initGenesis(s.chainA) + s.initGenesis(s.chainA, vestingMnemonic) s.initValidatorConfigs(s.chainA) s.runValidators(s.chainA, 0) s.T().Logf("starting e2e infrastructure for chain B; chain-id: %s; datadir: %s", s.chainB.id, s.chainB.dataDir) s.initNodes(s.chainB) - s.initGenesis(s.chainB) + s.initGenesis(s.chainB, vestingMnemonic) s.initValidatorConfigs(s.chainB) s.runValidators(s.chainB, 10) @@ -209,20 +233,89 @@ func (s *IntegrationTestSuite) initNodes(c *chain) { } } -func (s *IntegrationTestSuite) initGenesis(c *chain) { - serverCtx := server.NewDefaultContext() - config := serverCtx.Config +// TODO find a better way to manipulate accounts to add genesis accounts +func (s *IntegrationTestSuite) generateAuthAndBankState( + c *chain, + vestingMnemonic string, + appGenState map[string]json.RawMessage, +) ([]byte, []byte) { + var ( + authGenState = authtypes.GetGenesisStateFromAppState(cdc, appGenState) + bankGenState = banktypes.GetGenesisStateFromAppState(cdc, appGenState) + valConfigDir = c.validators[0].configDir() + ) + kb, err := keyring.New(keyringAppName, keyring.BackendTest, valConfigDir, nil, cdc) + s.Require().NoError(err) - config.SetRoot(c.validators[0].configDir()) - config.Moniker = c.validators[0].moniker + keyringAlgos, _ := kb.SupportedAlgorithms() + algo, err := keyring.NewSigningAlgoFromString(string(hd.Secp256k1Type), keyringAlgos) + s.Require().NoError(err) - genFilePath := config.GenesisFile() - appGenState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFilePath) + c.genesisVestingAccounts = make(map[string]sdk.AccAddress) + for i, key := range genesisVestingKeys { + // Use the first wallet from the same mnemonic by HD path + acc, err := kb.NewAccount(key, vestingMnemonic, "", HDPath(i), algo) + s.Require().NoError(err) + c.genesisVestingAccounts[key], err = acc.GetAddress() + s.Require().NoError(err) + s.T().Logf("created %s genesis account %s\n", key, c.genesisVestingAccounts[key].String()) + } + var ( + continuousVestingAcc = c.genesisVestingAccounts[continuousVestingKey] + delayedVestingAcc = c.genesisVestingAccounts[delayedVestingKey] + ) + + // add continuous vesting account to the genesis + baseVestingContinuousAccount := authtypes.NewBaseAccount( + continuousVestingAcc, nil, 0, 0) + vestingContinuousGenAccount := authvesting.NewContinuousVestingAccountRaw( + authvesting.NewBaseVestingAccount( + baseVestingContinuousAccount, + sdk.NewCoins(vestingAmountVested), + time.Now().Add(time.Duration(rand.Intn(80)+150)*time.Second).Unix(), + ), + time.Now().Add(time.Duration(rand.Intn(40)+90)*time.Second).Unix(), + ) + s.Require().NoError(vestingContinuousGenAccount.Validate()) + + // add delayed vesting account to the genesis + baseVestingDelayedAccount := authtypes.NewBaseAccount( + delayedVestingAcc, nil, 0, 0) + vestingDelayedGenAccount := authvesting.NewDelayedVestingAccountRaw( + authvesting.NewBaseVestingAccount( + baseVestingDelayedAccount, + sdk.NewCoins(vestingAmountVested), + time.Now().Add(time.Duration(rand.Intn(40)+90)*time.Second).Unix(), + ), + ) + s.Require().NoError(vestingDelayedGenAccount.Validate()) + + // unpack and append accounts + accs, err := authtypes.UnpackAccounts(authGenState.Accounts) + s.Require().NoError(err) + accs = append(accs, vestingContinuousGenAccount, vestingDelayedGenAccount) + accs = authtypes.SanitizeGenesisAccounts(accs) + genAccs, err := authtypes.PackAccounts(accs) + s.Require().NoError(err) + authGenState.Accounts = genAccs + + // update auth module state + auth, err := cdc.MarshalJSON(&authGenState) s.Require().NoError(err) - var bankGenState banktypes.GenesisState - s.Require().NoError(cdc.UnmarshalJSON(appGenState[banktypes.ModuleName], &bankGenState)) + // update balances + vestingContinuousBalances := banktypes.Balance{ + Address: continuousVestingAcc.String(), + Coins: vestingBalance, + } + vestingDelayedBalances := banktypes.Balance{ + Address: delayedVestingAcc.String(), + Coins: vestingBalance, + } + bankGenState.Balances = append(bankGenState.Balances, vestingContinuousBalances, vestingDelayedBalances) + bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances) + // update the denom metadata for the bank module bankGenState.DenomMetadata = append(bankGenState.DenomMetadata, banktypes.Metadata{ Description: "An example stable token", Display: uatomDenom, @@ -237,9 +330,27 @@ func (s *IntegrationTestSuite) initGenesis(c *chain) { }, }) - bz, err := cdc.MarshalJSON(&bankGenState) + // update bank module state + bank, err := cdc.MarshalJSON(bankGenState) s.Require().NoError(err) - appGenState[banktypes.ModuleName] = bz + + return bank, auth +} + +func (s *IntegrationTestSuite) initGenesis(c *chain, vestingMnemonic string) { + serverCtx := server.NewDefaultContext() + config := serverCtx.Config + + config.SetRoot(c.validators[0].configDir()) + config.Moniker = c.validators[0].moniker + + genFilePath := config.GenesisFile() + appGenState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFilePath) + s.Require().NoError(err) + + bankGenState, authGenState := s.generateAuthAndBankState(c, vestingMnemonic, appGenState) + appGenState[authtypes.ModuleName] = authGenState + appGenState[banktypes.ModuleName] = bankGenState var genUtilGenState genutiltypes.GenesisState s.Require().NoError(cdc.UnmarshalJSON(appGenState[genutiltypes.ModuleName], &genUtilGenState)) @@ -261,7 +372,7 @@ func (s *IntegrationTestSuite) initGenesis(c *chain) { genUtilGenState.GenTxs = genTxs - bz, err = cdc.MarshalJSON(&genUtilGenState) + bz, err := cdc.MarshalJSON(&genUtilGenState) s.Require().NoError(err) appGenState[genutiltypes.ModuleName] = bz @@ -273,10 +384,16 @@ func (s *IntegrationTestSuite) initGenesis(c *chain) { bz, err = tmjson.MarshalIndent(genDoc, "", " ") s.Require().NoError(err) + vestingPeriod, err := generateVestingPeriod() + s.Require().NoError(err) + // write the updated genesis file to each validator. for _, val := range c.validators { err = writeFile(filepath.Join(val.configDir(), "config", "genesis.json"), bz) s.Require().NoError(err) + + err = writeFile(filepath.Join(val.configDir(), vestingPeriodFilePath), vestingPeriod) + s.Require().NoError(err) } } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 9e861f90..d029fd9a 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -411,3 +411,11 @@ func (s *IntegrationTestSuite) TestStaking() { func (s *IntegrationTestSuite) TestGroups() { s.GroupsSendMsgTest() } + +func (s *IntegrationTestSuite) TestVesting() { + chainAAPI := fmt.Sprintf("http://%s", s.valResources[s.chainA.id][0].GetHostPort("1317/tcp")) + s.testDelayedVestingAccount(chainAAPI) + s.testContinuousVestingAccount(chainAAPI) + s.testPermanentLockedAccount(chainAAPI) + s.testPeriodicVestingAccount(chainAAPI) +} diff --git a/tests/e2e/e2e_vesting_test.go b/tests/e2e/e2e_vesting_test.go new file mode 100644 index 00000000..ecf925f3 --- /dev/null +++ b/tests/e2e/e2e_vesting_test.go @@ -0,0 +1,389 @@ +package e2e + +import ( + "cosmossdk.io/math" + "encoding/json" + "math/rand" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + delayedVestingKey = "delayed_vesting" + continuousVestingKey = "continuous_vesting" + lockedVestingKey = "locker_vesting" + periodicVestingKey = "periodic_vesting" + + vestingPeriodFilePath = "test_period.json" + vestingTxDelay = 5 +) + +type ( + vestingPeriod struct { + StartTime int64 `json:"start_time"` + Periods []period `json:"periods"` + } + period struct { + Coins string `json:"coins"` + Length int64 `json:"length_seconds"` + } +) + +var ( + genesisVestingKeys = []string{continuousVestingKey, delayedVestingKey, lockedVestingKey, periodicVestingKey} + vestingAmountVested = sdk.NewCoin(uatomDenom, math.NewInt(99900000000)) + vestingAmount = sdk.NewCoin(uatomDenom, math.NewInt(350000)) + vestingBalance = sdk.NewCoins(vestingAmountVested).Add(vestingAmount) + vestingDelegationAmount = sdk.NewCoin(uatomDenom, sdk.NewInt(500000000)) + vestingDelegationFees = sdk.NewCoin(uatomDenom, sdk.NewInt(10)) +) + +func (s *IntegrationTestSuite) testDelayedVestingAccount(api string) { + var ( + valIdx = 0 + chain = s.chainA + val = chain.validators[valIdx] + vestingDelayedAcc = chain.genesisVestingAccounts[delayedVestingKey] + ) + sender, err := val.keyInfo.GetAddress() + s.NoError(err) + valOpAddr := sdk.ValAddress(sender).String() + + s.Run("test delayed vesting genesis account", func() { + acc, err := queryDelayedVestingAccount(api, vestingDelayedAcc.String()) + s.Require().NoError(err) + + // Check address balance + balance, err := getSpecificBalance(api, vestingDelayedAcc.String(), uatomDenom) + s.Require().NoError(err) + s.Require().Equal(vestingBalance.AmountOf(uatomDenom), balance.Amount) + + // Delegate coins should succeed + s.executeDelegate(chain, valIdx, vestingDelegationAmount.String(), valOpAddr, + vestingDelayedAcc.String(), gaiaHomePath, vestingDelegationFees.String()) + + // Validate delegation successful + s.Require().Eventually( + func() bool { + res, err := queryDelegation(api, valOpAddr, vestingDelayedAcc.String()) + amt := res.GetDelegationResponse().GetDelegation().GetShares() + s.Require().NoError(err) + + return amt.Equal(sdk.NewDecFromInt(vestingDelegationAmount.Amount)) + }, + 20*time.Second, + 5*time.Second, + ) + + waitTime := acc.EndTime - time.Now().Unix() + if waitTime > vestingTxDelay { + // Transfer coins should fail + balance, err := getSpecificBalance(api, vestingDelayedAcc.String(), uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + vestingDelayedAcc.String(), + Address(), + balance.Sub(fees).String(), + fees.String(), + true, + ) + waitTime = acc.EndTime - time.Now().Unix() + vestingTxDelay + time.Sleep(time.Duration(waitTime) * time.Second) + } + + // Transfer coins should succeed + balance, err = getSpecificBalance(api, vestingDelayedAcc.String(), uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + vestingDelayedAcc.String(), + Address(), + balance.Sub(fees).String(), + fees.String(), + false, + ) + }) +} + +func (s *IntegrationTestSuite) testContinuousVestingAccount(api string) { + s.Run("test continuous vesting genesis account", func() { + var ( + valIdx = 0 + chain = s.chainA + val = chain.validators[valIdx] + continuousVestingAcc = chain.genesisVestingAccounts[continuousVestingKey] + ) + sender, err := val.keyInfo.GetAddress() + s.NoError(err) + valOpAddr := sdk.ValAddress(sender).String() + + acc, err := queryContinuousVestingAccount(api, continuousVestingAcc.String()) + s.Require().NoError(err) + + // Check address balance + balance, err := getSpecificBalance(api, continuousVestingAcc.String(), uatomDenom) + s.Require().NoError(err) + s.Require().Equal(vestingBalance.AmountOf(uatomDenom), balance.Amount) + + // Delegate coins should succeed + s.executeDelegate(chain, valIdx, vestingDelegationAmount.String(), + valOpAddr, continuousVestingAcc.String(), gaiaHomePath, vestingDelegationFees.String()) + + // Validate delegation successful + s.Require().Eventually( + func() bool { + res, err := queryDelegation(api, valOpAddr, continuousVestingAcc.String()) + amt := res.GetDelegationResponse().GetDelegation().GetShares() + s.Require().NoError(err) + + return amt.Equal(sdk.NewDecFromInt(vestingDelegationAmount.Amount)) + }, + 20*time.Second, + 5*time.Second, + ) + + waitStartTime := acc.StartTime - time.Now().Unix() + if waitStartTime > vestingTxDelay { + // Transfer coins should fail + balance, err := getSpecificBalance(api, continuousVestingAcc.String(), uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + continuousVestingAcc.String(), + Address(), + balance.Sub(fees).String(), + fees.String(), + true, + ) + waitStartTime = acc.StartTime - time.Now().Unix() + vestingTxDelay + time.Sleep(time.Duration(waitStartTime) * time.Second) + } + + waitEndTime := acc.EndTime - time.Now().Unix() + if waitEndTime > vestingTxDelay { + // Transfer coins should fail + balance, err := getSpecificBalance(api, continuousVestingAcc.String(), uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + continuousVestingAcc.String(), + Address(), + balance.Sub(fees).String(), + fees.String(), + true, + ) + waitEndTime = acc.EndTime - time.Now().Unix() + vestingTxDelay + time.Sleep(time.Duration(waitEndTime) * time.Second) + } + + // Transfer coins should succeed + balance, err = getSpecificBalance(api, continuousVestingAcc.String(), uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + continuousVestingAcc.String(), + Address(), + balance.Sub(fees).String(), + fees.String(), + false, + ) + }) +} + +func (s *IntegrationTestSuite) testPermanentLockedAccount(api string) { + s.Run("test permanent locked vesting genesis account", func() { + var ( + valIdx = 0 + chain = s.chainA + val = chain.validators[valIdx] + permanentLockedAddr = chain.genesisVestingAccounts[lockedVestingKey].String() + ) + sender, err := val.keyInfo.GetAddress() + s.NoError(err) + valOpAddr := sdk.ValAddress(sender).String() + + s.execCreatePermanentLockedAccount(chain, permanentLockedAddr, + vestingAmountVested.String(), withKeyValue(flagFrom, sender.String()), + ) + + _, err = queryPermanentLockedAccount(api, permanentLockedAddr) + s.Require().NoError(err) + + // Check address balance + balance, err := getSpecificBalance(api, permanentLockedAddr, uatomDenom) + s.Require().NoError(err) + s.Require().Equal(vestingAmountVested.Amount, balance.Amount) + + // Transfer coins to pay the delegation fee + s.execBankSend(chain, valIdx, sender.String(), permanentLockedAddr, + fees.String(), fees.String(), false) + + // Delegate coins should succeed + s.executeDelegate(chain, valIdx, vestingDelegationAmount.String(), valOpAddr, + permanentLockedAddr, gaiaHomePath, vestingDelegationFees.String()) + + // Validate delegation successful + s.Require().Eventually( + func() bool { + res, err := queryDelegation(api, valOpAddr, permanentLockedAddr) + amt := res.GetDelegationResponse().GetDelegation().GetShares() + s.Require().NoError(err) + + return amt.Equal(sdk.NewDecFromInt(vestingDelegationAmount.Amount)) + }, + 20*time.Second, + 5*time.Second, + ) + + // Transfer coins should fail + balance, err = getSpecificBalance(api, permanentLockedAddr, uatomDenom) + s.Require().NoError(err) + s.execBankSend(chain, valIdx, permanentLockedAddr, Address(), + balance.Sub(fees).String(), fees.String(), true) + }) +} + +func (s *IntegrationTestSuite) testPeriodicVestingAccount(api string) { + s.Run("test periodic vesting genesis account", func() { + var ( + valIdx = 0 + chain = s.chainA + val = chain.validators[valIdx] + periodicVestingAddr = chain.genesisVestingAccounts[periodicVestingKey].String() + ) + sender, err := val.keyInfo.GetAddress() + s.NoError(err) + valOpAddr := sdk.ValAddress(sender).String() + + s.execCreatePeriodicVestingAccount( + chain, + periodicVestingAddr, + withKeyValue(flagFrom, sender.String()), + ) + + acc, err := queryPeriodicVestingAccount(api, periodicVestingAddr) + s.Require().NoError(err) + + // Check address balance + balance, err := getSpecificBalance(api, periodicVestingAddr, uatomDenom) + s.Require().NoError(err) + + expectedBalance := sdk.NewCoin(uatomDenom, sdk.NewInt(0)) + for _, period := range acc.VestingPeriods { + _, coin := period.Amount.Find(uatomDenom) + expectedBalance = expectedBalance.Add(coin) + } + s.Require().Equal(expectedBalance, balance) + + waitStartTime := acc.StartTime - time.Now().Unix() + if waitStartTime > vestingTxDelay { + // Transfer coins should fail + balance, err = getSpecificBalance(api, periodicVestingAddr, uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + periodicVestingAddr, + Address(), + balance.Sub(fees).String(), + fees.String(), + true, + ) + waitStartTime = acc.StartTime - time.Now().Unix() + vestingTxDelay + time.Sleep(time.Duration(waitStartTime) * time.Second) + } + + firstPeriod := acc.StartTime + acc.VestingPeriods[0].Length + waitFirstPeriod := firstPeriod - time.Now().Unix() + if waitFirstPeriod > vestingTxDelay { + // Transfer coins should fail + balance, err = getSpecificBalance(api, periodicVestingAddr, uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + periodicVestingAddr, + Address(), + balance.Sub(fees).String(), + fees.String(), + true, + ) + waitFirstPeriod = firstPeriod - time.Now().Unix() + vestingTxDelay + time.Sleep(time.Duration(waitFirstPeriod) * time.Second) + } + + // Delegate coins should succeed + s.executeDelegate(chain, valIdx, vestingDelegationAmount.String(), valOpAddr, + periodicVestingAddr, gaiaHomePath, vestingDelegationFees.String()) + + // Validate delegation successful + s.Require().Eventually( + func() bool { + res, err := queryDelegation(api, valOpAddr, periodicVestingAddr) + amt := res.GetDelegationResponse().GetDelegation().GetShares() + s.Require().NoError(err) + + return amt.Equal(sdk.NewDecFromInt(vestingDelegationAmount.Amount)) + }, + 20*time.Second, + 5*time.Second, + ) + + // Transfer coins should succeed + balance, err = getSpecificBalance(api, periodicVestingAddr, uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + periodicVestingAddr, + Address(), + balance.Sub(fees).String(), + fees.String(), + false, + ) + + secondPeriod := firstPeriod + acc.VestingPeriods[1].Length + waitSecondPeriod := secondPeriod - time.Now().Unix() + if waitSecondPeriod > vestingTxDelay { + time.Sleep(time.Duration(waitSecondPeriod) * time.Second) + + // Transfer coins should succeed + balance, err = getSpecificBalance(api, periodicVestingAddr, uatomDenom) + s.Require().NoError(err) + s.execBankSend( + chain, + valIdx, + periodicVestingAddr, + Address(), + balance.Sub(fees).String(), + fees.String(), + false, + ) + } + }) +} + +// generateVestingPeriod generate the vesting period file +func generateVestingPeriod() ([]byte, error) { + p := vestingPeriod{ + StartTime: time.Now().Add(time.Duration(rand.Intn(20)+95) * time.Second).Unix(), + Periods: []period{ + { + Coins: "850000000" + uatomDenom, + Length: 35, + }, + { + Coins: "2000000000" + uatomDenom, + Length: 35, + }, + }, + } + return json.Marshal(p) +} diff --git a/tests/e2e/query.go b/tests/e2e/query.go index eeccb23d..2ec8ea82 100644 --- a/tests/e2e/query.go +++ b/tests/e2e/query.go @@ -8,6 +8,8 @@ import ( "strings" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types" govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" @@ -241,3 +243,72 @@ func queryGovProposal(endpoint string, proposalID int) (govv1beta1.QueryProposal return govProposalResp, nil } + +func queryAccount(endpoint, address string) (acc authtypes.AccountI, err error) { + var res authtypes.QueryAccountResponse + resp, err := http.Get(fmt.Sprintf("%s/cosmos/auth/v1beta1/accounts/%s", endpoint, address)) + if err != nil { + return nil, fmt.Errorf("failed to execute HTTP request: %w", err) + } + defer resp.Body.Close() + + bz, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if err := cdc.UnmarshalJSON(bz, &res); err != nil { + return nil, err + } + return acc, cdc.UnpackAny(res.Account, &acc) +} + +func queryDelayedVestingAccount(endpoint, address string) (authvesting.DelayedVestingAccount, error) { + baseAcc, err := queryAccount(endpoint, address) + if err != nil { + return authvesting.DelayedVestingAccount{}, err + } + acc, ok := baseAcc.(*authvesting.DelayedVestingAccount) + if !ok { + return authvesting.DelayedVestingAccount{}, + fmt.Errorf("cannot cast %v to DelayedVestingAccount", baseAcc) + } + return *acc, nil +} + +func queryContinuousVestingAccount(endpoint, address string) (authvesting.ContinuousVestingAccount, error) { + baseAcc, err := queryAccount(endpoint, address) + if err != nil { + return authvesting.ContinuousVestingAccount{}, err + } + acc, ok := baseAcc.(*authvesting.ContinuousVestingAccount) + if !ok { + return authvesting.ContinuousVestingAccount{}, + fmt.Errorf("cannot cast %v to ContinuousVestingAccount", baseAcc) + } + return *acc, nil +} +func queryPermanentLockedAccount(endpoint, address string) (authvesting.PermanentLockedAccount, error) { + baseAcc, err := queryAccount(endpoint, address) + if err != nil { + return authvesting.PermanentLockedAccount{}, err + } + acc, ok := baseAcc.(*authvesting.PermanentLockedAccount) + if !ok { + return authvesting.PermanentLockedAccount{}, + fmt.Errorf("cannot cast %v to PermanentLockedAccount", baseAcc) + } + return *acc, nil +} + +func queryPeriodicVestingAccount(endpoint, address string) (authvesting.PeriodicVestingAccount, error) { + baseAcc, err := queryAccount(endpoint, address) + if err != nil { + return authvesting.PeriodicVestingAccount{}, err + } + acc, ok := baseAcc.(*authvesting.PeriodicVestingAccount) + if !ok { + return authvesting.PeriodicVestingAccount{}, + fmt.Errorf("cannot cast %v to PeriodicVestingAccount", baseAcc) + } + return *acc, nil +}