mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25:18 +00:00
harvest v1 (#658)
* wip: and types and keeper methods * add keeper tests * add client * add spec and events * respond to review comments * apply suggestions from review * feat: add test for validator vesting case * use int64 for multiplier type * remove incentive changes
This commit is contained in:
parent
7292b8843a
commit
fe38c4aa43
21
app/app.go
21
app/app.go
@ -37,6 +37,7 @@ import (
|
||||
"github.com/kava-labs/kava/x/bep3"
|
||||
"github.com/kava-labs/kava/x/cdp"
|
||||
"github.com/kava-labs/kava/x/committee"
|
||||
"github.com/kava-labs/kava/x/hvt"
|
||||
"github.com/kava-labs/kava/x/incentive"
|
||||
"github.com/kava-labs/kava/x/issuance"
|
||||
"github.com/kava-labs/kava/x/kavadist"
|
||||
@ -82,6 +83,7 @@ var (
|
||||
kavadist.AppModuleBasic{},
|
||||
incentive.AppModuleBasic{},
|
||||
issuance.AppModuleBasic{},
|
||||
hvt.AppModuleBasic{},
|
||||
)
|
||||
|
||||
// module account permissions
|
||||
@ -100,6 +102,9 @@ var (
|
||||
bep3.ModuleName: {supply.Minter, supply.Burner},
|
||||
kavadist.ModuleName: {supply.Minter},
|
||||
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
|
||||
hvt.LPAccount: {supply.Minter, supply.Burner},
|
||||
hvt.DelegatorAccount: {supply.Minter, supply.Burner},
|
||||
hvt.ModuleAccountName: {supply.Minter, supply.Burner},
|
||||
}
|
||||
|
||||
// module accounts that are allowed to receive tokens
|
||||
@ -144,6 +149,7 @@ type App struct {
|
||||
kavadistKeeper kavadist.Keeper
|
||||
incentiveKeeper incentive.Keeper
|
||||
issuanceKeeper issuance.Keeper
|
||||
harvestKeeper hvt.Keeper
|
||||
|
||||
// the module manager
|
||||
mm *module.Manager
|
||||
@ -169,6 +175,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
|
||||
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
|
||||
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
|
||||
hvt.StoreKey,
|
||||
)
|
||||
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
|
||||
|
||||
@ -198,6 +205,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
|
||||
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
|
||||
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
|
||||
harvestSubspace := app.paramsKeeper.Subspace(hvt.DefaultParamspace)
|
||||
|
||||
// add keepers
|
||||
app.accountKeeper = auth.NewAccountKeeper(
|
||||
@ -362,6 +370,13 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
app.accountKeeper,
|
||||
app.supplyKeeper,
|
||||
)
|
||||
app.harvestKeeper = hvt.NewKeeper(
|
||||
app.cdc,
|
||||
keys[hvt.StoreKey],
|
||||
harvestSubspace,
|
||||
app.accountKeeper,
|
||||
app.supplyKeeper,
|
||||
&stakingKeeper)
|
||||
|
||||
// register the staking hooks
|
||||
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
|
||||
@ -392,6 +407,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
||||
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
||||
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
||||
hvt.NewAppModule(app.harvestKeeper, app.supplyKeeper),
|
||||
)
|
||||
|
||||
// During begin block slashing happens after distr.BeginBlocker so that
|
||||
@ -402,7 +418,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
app.mm.SetOrderBeginBlockers(
|
||||
upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName,
|
||||
validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName,
|
||||
bep3.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
|
||||
bep3.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName, hvt.ModuleName,
|
||||
)
|
||||
|
||||
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
|
||||
@ -413,7 +429,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
||||
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
|
||||
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
|
||||
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
|
||||
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName, hvt.ModuleName,
|
||||
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
|
||||
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
|
||||
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
|
||||
@ -444,6 +460,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
|
||||
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
|
||||
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
|
||||
hvt.NewAppModule(app.harvestKeeper, app.supplyKeeper),
|
||||
)
|
||||
|
||||
app.sm.RegisterStoreDecoders()
|
||||
|
@ -34,6 +34,7 @@ import (
|
||||
"github.com/kava-labs/kava/x/bep3"
|
||||
"github.com/kava-labs/kava/x/cdp"
|
||||
"github.com/kava-labs/kava/x/committee"
|
||||
"github.com/kava-labs/kava/x/hvt"
|
||||
"github.com/kava-labs/kava/x/incentive"
|
||||
"github.com/kava-labs/kava/x/issuance"
|
||||
"github.com/kava-labs/kava/x/kavadist"
|
||||
@ -84,10 +85,11 @@ func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefee
|
||||
func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
|
||||
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
|
||||
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
|
||||
func (tApp TestApp) GetHarvestKeeper() hvt.Keeper { return tApp.harvestKeeper }
|
||||
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
|
||||
func (tApp TestApp) GetIssuanceKeeper() issuance.Keeper { return tApp.issuanceKeeper }
|
||||
|
||||
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
||||
// InitializeFromGenesisStates calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
||||
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
|
||||
// Create a default genesis state and overwrite with provided values
|
||||
genesisState := NewDefaultGenesisState()
|
||||
|
15
x/hvt/abci.go
Normal file
15
x/hvt/abci.go
Normal file
@ -0,0 +1,15 @@
|
||||
package hvt
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// BeginBlocker applies rewards to liquidity providers and delegators according to params
|
||||
func BeginBlocker(ctx sdk.Context, k Keeper) {
|
||||
k.ApplyDepositRewards(ctx)
|
||||
if k.ShouldDistributeValidatorRewards(ctx, k.BondDenom(ctx)) {
|
||||
k.ApplyDelegationRewards(ctx, k.BondDenom(ctx))
|
||||
k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), k.BondDenom(ctx))
|
||||
}
|
||||
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
|
||||
}
|
134
x/hvt/alias.go
Normal file
134
x/hvt/alias.go
Normal file
@ -0,0 +1,134 @@
|
||||
package hvt
|
||||
|
||||
// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen)
|
||||
|
||||
import (
|
||||
"github.com/kava-labs/kava/x/hvt/keeper"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
const (
|
||||
BeginningOfMonth = keeper.BeginningOfMonth
|
||||
MidMonth = keeper.MidMonth
|
||||
PaymentHour = keeper.PaymentHour
|
||||
AttributeKeyBlockHeight = types.AttributeKeyBlockHeight
|
||||
AttributeKeyClaimAmount = types.AttributeKeyClaimAmount
|
||||
AttributeKeyClaimHolder = types.AttributeKeyClaimHolder
|
||||
AttributeKeyClaimMultiplier = types.AttributeKeyClaimMultiplier
|
||||
AttributeKeyDeposit = types.AttributeKeyDeposit
|
||||
AttributeKeyDepositDenom = types.AttributeKeyDepositDenom
|
||||
AttributeKeyDepositType = types.AttributeKeyDepositType
|
||||
AttributeKeyDepositor = types.AttributeKeyDepositor
|
||||
AttributeKeyRewardsDistribution = types.AttributeKeyRewardsDistribution
|
||||
AttributeValueCategory = types.AttributeValueCategory
|
||||
DefaultParamspace = types.DefaultParamspace
|
||||
DelegatorAccount = types.DelegatorAccount
|
||||
EventTypeClaimHarvestReward = types.EventTypeClaimHarvestReward
|
||||
EventTypeDeleteHarvestDeposit = types.EventTypeDeleteHarvestDeposit
|
||||
EventTypeHarvestDelegatorDistribution = types.EventTypeHarvestDelegatorDistribution
|
||||
EventTypeHarvestDeposit = types.EventTypeHarvestDeposit
|
||||
EventTypeHarvestLPDistribution = types.EventTypeHarvestLPDistribution
|
||||
EventTypeHarvestWithdrawal = types.EventTypeHarvestWithdrawal
|
||||
LP = types.LP
|
||||
LPAccount = types.LPAccount
|
||||
Large = types.Large
|
||||
Medium = types.Medium
|
||||
ModuleAccountName = types.ModuleAccountName
|
||||
ModuleName = types.ModuleName
|
||||
QuerierRoute = types.QuerierRoute
|
||||
QueryGetClaims = types.QueryGetClaims
|
||||
QueryGetDeposits = types.QueryGetDeposits
|
||||
QueryGetModuleAccounts = types.QueryGetModuleAccounts
|
||||
QueryGetParams = types.QueryGetParams
|
||||
RouterKey = types.RouterKey
|
||||
Small = types.Small
|
||||
Stake = types.Stake
|
||||
StoreKey = types.StoreKey
|
||||
)
|
||||
|
||||
var (
|
||||
// function aliases
|
||||
NewKeeper = keeper.NewKeeper
|
||||
NewQuerier = keeper.NewQuerier
|
||||
ClaimKey = types.ClaimKey
|
||||
DefaultGenesisState = types.DefaultGenesisState
|
||||
DefaultParams = types.DefaultParams
|
||||
DepositKey = types.DepositKey
|
||||
DepositTypeIteratorKey = types.DepositTypeIteratorKey
|
||||
GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength
|
||||
NewClaim = types.NewClaim
|
||||
NewDelegatorDistributionSchedule = types.NewDelegatorDistributionSchedule
|
||||
NewDeposit = types.NewDeposit
|
||||
NewDistributionSchedule = types.NewDistributionSchedule
|
||||
NewGenesisState = types.NewGenesisState
|
||||
NewMsgClaimReward = types.NewMsgClaimReward
|
||||
NewMsgDeposit = types.NewMsgDeposit
|
||||
NewMsgWithdraw = types.NewMsgWithdraw
|
||||
NewMultiplier = types.NewMultiplier
|
||||
NewParams = types.NewParams
|
||||
NewPeriod = types.NewPeriod
|
||||
NewQueryAccountParams = types.NewQueryAccountParams
|
||||
NewQueryClaimParams = types.NewQueryClaimParams
|
||||
NewQueryDepositParams = types.NewQueryDepositParams
|
||||
ParamKeyTable = types.ParamKeyTable
|
||||
RegisterCodec = types.RegisterCodec
|
||||
|
||||
// variable aliases
|
||||
ClaimsKeyPrefix = types.ClaimsKeyPrefix
|
||||
DefaultActive = types.DefaultActive
|
||||
DefaultDelegatorSchedules = types.DefaultDelegatorSchedules
|
||||
DefaultDistributionTimes = types.DefaultDistributionTimes
|
||||
DefaultGovSchedules = types.DefaultGovSchedules
|
||||
DefaultLPSchedules = types.DefaultLPSchedules
|
||||
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
|
||||
DepositTypesClaimQuery = types.DepositTypesClaimQuery
|
||||
DepositTypesDepositQuery = types.DepositTypesDepositQuery
|
||||
DepositsKeyPrefix = types.DepositsKeyPrefix
|
||||
ErrAccountNotFound = types.ErrAccountNotFound
|
||||
ErrClaimExpired = types.ErrClaimExpired
|
||||
ErrClaimNotFound = types.ErrClaimNotFound
|
||||
ErrDepositNotFound = types.ErrDepositNotFound
|
||||
ErrGovScheduleNotFound = types.ErrGovScheduleNotFound
|
||||
ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance
|
||||
ErrInvaliWithdrawAmount = types.ErrInvaliWithdrawAmount
|
||||
ErrInvalidAccountType = types.ErrInvalidAccountType
|
||||
ErrInvalidDepositDenom = types.ErrInvalidDepositDenom
|
||||
ErrInvalidDepositType = types.ErrInvalidDepositType
|
||||
ErrInvalidMultiplier = types.ErrInvalidMultiplier
|
||||
ErrLPScheduleNotFound = types.ErrLPScheduleNotFound
|
||||
ErrZeroClaim = types.ErrZeroClaim
|
||||
GovDenom = types.GovDenom
|
||||
KeyActive = types.KeyActive
|
||||
KeyDelegatorSchedule = types.KeyDelegatorSchedule
|
||||
KeyLPSchedules = types.KeyLPSchedules
|
||||
ModuleCdc = types.ModuleCdc
|
||||
PreviousBlockTimeKey = types.PreviousBlockTimeKey
|
||||
PreviousDelegationDistributionKey = types.PreviousDelegationDistributionKey
|
||||
)
|
||||
|
||||
type (
|
||||
Keeper = keeper.Keeper
|
||||
AccountKeeper = types.AccountKeeper
|
||||
Claim = types.Claim
|
||||
DelegatorDistributionSchedule = types.DelegatorDistributionSchedule
|
||||
DelegatorDistributionSchedules = types.DelegatorDistributionSchedules
|
||||
Deposit = types.Deposit
|
||||
DepositType = types.DepositType
|
||||
DistributionSchedule = types.DistributionSchedule
|
||||
DistributionSchedules = types.DistributionSchedules
|
||||
GenesisDistributionTime = types.GenesisDistributionTime
|
||||
GenesisDistributionTimes = types.GenesisDistributionTimes
|
||||
GenesisState = types.GenesisState
|
||||
MsgClaimReward = types.MsgClaimReward
|
||||
MsgDeposit = types.MsgDeposit
|
||||
MsgWithdraw = types.MsgWithdraw
|
||||
Multiplier = types.Multiplier
|
||||
MultiplierName = types.MultiplierName
|
||||
Multipliers = types.Multipliers
|
||||
Params = types.Params
|
||||
QueryAccountParams = types.QueryAccountParams
|
||||
QueryClaimParams = types.QueryClaimParams
|
||||
QueryDepositParams = types.QueryDepositParams
|
||||
StakingKeeper = types.StakingKeeper
|
||||
SupplyKeeper = types.SupplyKeeper
|
||||
)
|
240
x/hvt/client/cli/query.go
Normal file
240
x/hvt/client/cli/query.go
Normal file
@ -0,0 +1,240 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// flags for cli queries
|
||||
const (
|
||||
flagName = "name"
|
||||
flagDepositDenom = "deposit-denom"
|
||||
flagOwner = "owner"
|
||||
flagDepositType = "deposit-type"
|
||||
)
|
||||
|
||||
// GetQueryCmd returns the cli query commands for the harvest module
|
||||
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
harvestQueryCmd := &cobra.Command{
|
||||
Use: types.ModuleName,
|
||||
Short: "Querying commands for the harvest module",
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
RunE: client.ValidateCmd,
|
||||
}
|
||||
|
||||
harvestQueryCmd.AddCommand(flags.GetCommands(
|
||||
queryParamsCmd(queryRoute, cdc),
|
||||
queryModAccountsCmd(queryRoute, cdc),
|
||||
queryDepositsCmd(queryRoute, cdc),
|
||||
queryClaimsCmd(queryRoute, cdc),
|
||||
)...)
|
||||
|
||||
return harvestQueryCmd
|
||||
|
||||
}
|
||||
|
||||
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "params",
|
||||
Short: "get the harvest module parameters",
|
||||
Long: "Get the current global harvest module parameters.",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
|
||||
// Query
|
||||
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
|
||||
res, height, err := cliCtx.QueryWithData(route, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
|
||||
// Decode and print results
|
||||
var params types.Params
|
||||
if err := cdc.UnmarshalJSON(res, ¶ms); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
return cliCtx.PrintOutput(params)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func queryModAccountsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "accounts",
|
||||
Short: "query harvest module accounts with optional filter",
|
||||
Long: strings.TrimSpace(`Query for all harvest module accounts or a specific account using the name flag:
|
||||
|
||||
Example:
|
||||
$ kvcli q harvest accounts
|
||||
$ kvcli q harvest accounts --name harvest|harvest_delegator_distribution|harvest_lp_distribution`,
|
||||
),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
|
||||
name := viper.GetString(flagName)
|
||||
page := viper.GetInt(flags.FlagPage)
|
||||
limit := viper.GetInt(flags.FlagLimit)
|
||||
|
||||
params := types.NewQueryAccountParams(page, limit, name)
|
||||
bz, err := cdc.MarshalJSON(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetModuleAccounts)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
|
||||
var modAccounts []supplyexported.ModuleAccountI
|
||||
if err := cdc.UnmarshalJSON(res, &modAccounts); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal module accounts: %w", err)
|
||||
}
|
||||
return cliCtx.PrintOutput(modAccounts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func queryDepositsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "deposits",
|
||||
Short: "query harvest module deposits with optional filters",
|
||||
Long: strings.TrimSpace(`query for all harvest module deposits or a specific deposit using flags:
|
||||
|
||||
Example:
|
||||
$ kvcli q harvest deposits
|
||||
$ kvcli q harvest deposits --owner kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny --deposit-type lp --deposit-denom bnb
|
||||
$ kvcli q harvest deposits --deposit-type stake --deposit-denom ukava
|
||||
$ kvcli q harvest deposits --deposit-denom btcb`,
|
||||
),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
|
||||
var owner sdk.AccAddress
|
||||
var depositType types.DepositType
|
||||
|
||||
ownerBech := viper.GetString(flagOwner)
|
||||
depositDenom := viper.GetString(flagDepositDenom)
|
||||
depositTypeStr := viper.GetString(flagDepositType)
|
||||
|
||||
if len(ownerBech) != 0 {
|
||||
depositOwner, err := sdk.AccAddressFromBech32(ownerBech)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owner = depositOwner
|
||||
}
|
||||
|
||||
if len(depositTypeStr) != 0 {
|
||||
if err := types.DepositType(depositTypeStr).IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
depositType = types.DepositType(depositTypeStr)
|
||||
}
|
||||
|
||||
page := viper.GetInt(flags.FlagPage)
|
||||
limit := viper.GetInt(flags.FlagLimit)
|
||||
|
||||
params := types.NewQueryDepositParams(page, limit, depositDenom, owner, depositType)
|
||||
bz, err := cdc.MarshalJSON(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetDeposits)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
|
||||
var deposits []types.Deposit
|
||||
if err := cdc.UnmarshalJSON(res, &deposits); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal deposits: %w", err)
|
||||
}
|
||||
return cliCtx.PrintOutput(deposits)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "claims",
|
||||
Short: "query harvest module claims with optional filters",
|
||||
Long: strings.TrimSpace(`query for all harvest module claims or a specific claim using flags:
|
||||
|
||||
Example:
|
||||
$ kvcli q harvest claims
|
||||
$ kvcli q harvest claims --owner kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny --deposit-type lp --deposit-denom bnb
|
||||
$ kvcli q harvest claims --deposit-type stake --deposit-denom ukava
|
||||
$ kvcli q harvest claims --deposit-denom btcb`,
|
||||
),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
|
||||
var owner sdk.AccAddress
|
||||
var depositType types.DepositType
|
||||
|
||||
ownerBech := viper.GetString(flagOwner)
|
||||
depositDenom := viper.GetString(flagDepositDenom)
|
||||
depositTypeStr := viper.GetString(flagDepositType)
|
||||
|
||||
if len(ownerBech) != 0 {
|
||||
claimOwner, err := sdk.AccAddressFromBech32(ownerBech)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owner = claimOwner
|
||||
}
|
||||
|
||||
if len(depositTypeStr) != 0 {
|
||||
if err := types.DepositType(depositTypeStr).IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
depositType = types.DepositType(depositTypeStr)
|
||||
}
|
||||
|
||||
page := viper.GetInt(flags.FlagPage)
|
||||
limit := viper.GetInt(flags.FlagLimit)
|
||||
|
||||
params := types.NewQueryDepositParams(page, limit, depositDenom, owner, depositType)
|
||||
bz, err := cdc.MarshalJSON(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetClaims)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
|
||||
var claims []types.Claim
|
||||
if err := cdc.UnmarshalJSON(res, &claims); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal claims: %w", err)
|
||||
}
|
||||
return cliCtx.PrintOutput(claims)
|
||||
},
|
||||
}
|
||||
}
|
118
x/hvt/client/cli/tx.go
Normal file
118
x/hvt/client/cli/tx.go
Normal file
@ -0,0 +1,118 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// GetTxCmd returns the transaction commands for this module
|
||||
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||
harvestTxCmd := &cobra.Command{
|
||||
Use: types.ModuleName,
|
||||
Short: fmt.Sprintf("%s transactions subcommands", types.ModuleName),
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
RunE: client.ValidateCmd,
|
||||
}
|
||||
|
||||
harvestTxCmd.AddCommand(flags.PostCommands(
|
||||
getCmdDeposit(cdc),
|
||||
getCmdWithdraw(cdc),
|
||||
getCmdClaimReward(cdc),
|
||||
)...)
|
||||
|
||||
return harvestTxCmd
|
||||
}
|
||||
|
||||
func getCmdDeposit(cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "deposit [amount] [deposit-type]",
|
||||
Short: "deposit coins to harvest",
|
||||
Args: cobra.ExactArgs(3),
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s deposit 10000000bnb lp --from <key>`, version.ClientName, types.ModuleName,
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||
amount, err := sdk.ParseCoin(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := types.NewMsgDeposit(cliCtx.GetFromAddress(), amount, args[1])
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCmdWithdraw(cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "withdraw [amount] [deposit-type]",
|
||||
Short: "withdraw coins from harvest",
|
||||
Args: cobra.ExactArgs(3),
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s withdraw 10000000bnb lp --from <key>`, version.ClientName, types.ModuleName,
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||
amount, err := sdk.ParseCoin(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := types.NewMsgWithdraw(cliCtx.GetFromAddress(), amount, args[1])
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCmdClaimReward(cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "claim [receiver-addr] [deposit-denom] [deposit-type] [multiplier]",
|
||||
Short: "claim HARD tokens to receiver address",
|
||||
Long: strings.TrimSpace(
|
||||
`sends accumulated HARD tokens from the harvest module account to the receiver address.
|
||||
Note that receiver address should match the sender address,
|
||||
unless the sender is a validator-vesting account`),
|
||||
Args: cobra.ExactArgs(4),
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s claim kava1hgcfsuwc889wtdmt8pjy7qffua9dd2tralu64j bnb lp large --from <key>`, version.ClientName, types.ModuleName,
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||
receiver, err := sdk.AccAddressFromBech32(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := types.NewMsgClaimReward(cliCtx.GetFromAddress(), receiver, args[2], args[3], args[4])
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||
},
|
||||
}
|
||||
}
|
196
x/hvt/client/rest/query.go
Normal file
196
x/hvt/client/rest/query.go
Normal file
@ -0,0 +1,196 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/deposits", types.ModuleName), queryDepositsHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/claims", types.ModuleName), queryClaimsHandlerFn(cliCtx)).Methods("GET")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/accounts", types.ModuleName), queryModAccountsHandlerFn(cliCtx)).Methods("GET")
|
||||
}
|
||||
|
||||
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/parameters", types.QuerierRoute)
|
||||
|
||||
res, height, err := cliCtx.QueryWithData(route, nil)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
rest.PostProcessResponse(w, cliCtx, res)
|
||||
}
|
||||
}
|
||||
|
||||
func queryDepositsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// Parse the query height
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var depositDenom string
|
||||
var depositOwner sdk.AccAddress
|
||||
var depositType types.DepositType
|
||||
|
||||
if x := r.URL.Query().Get(RestDenom); len(x) != 0 {
|
||||
depositDenom = strings.TrimSpace(x)
|
||||
}
|
||||
|
||||
if x := r.URL.Query().Get(RestOwner); len(x) != 0 {
|
||||
depositOwnerStr := strings.ToLower(strings.TrimSpace(x))
|
||||
depositOwner, err = sdk.AccAddressFromBech32(depositOwnerStr)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from deposit owner %s", depositOwnerStr))
|
||||
}
|
||||
}
|
||||
|
||||
if x := r.URL.Query().Get(RestType); len(x) != 0 {
|
||||
depositTypeStr := strings.ToLower(strings.TrimSpace(x))
|
||||
err := types.DepositType(depositTypeStr).IsValid()
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
depositType = types.DepositType(depositTypeStr)
|
||||
}
|
||||
|
||||
params := types.NewQueryDepositParams(page, limit, depositDenom, depositOwner, depositType)
|
||||
|
||||
bz, err := cliCtx.Codec.MarshalJSON(params)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetDeposits)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rest.PostProcessResponse(w, cliCtx, res)
|
||||
}
|
||||
}
|
||||
|
||||
func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// Parse the query height
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var depositDenom string
|
||||
var claimOwner sdk.AccAddress
|
||||
var depositType types.DepositType
|
||||
|
||||
if x := r.URL.Query().Get(RestDenom); len(x) != 0 {
|
||||
depositDenom = strings.TrimSpace(x)
|
||||
}
|
||||
|
||||
if x := r.URL.Query().Get(RestOwner); len(x) != 0 {
|
||||
claimOwnerStr := strings.ToLower(strings.TrimSpace(x))
|
||||
claimOwner, err = sdk.AccAddressFromBech32(claimOwnerStr)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from claim owner %s", claimOwnerStr))
|
||||
}
|
||||
}
|
||||
|
||||
if x := r.URL.Query().Get(RestType); len(x) != 0 {
|
||||
depositTypeStr := strings.ToLower(strings.TrimSpace(x))
|
||||
err := types.DepositType(depositTypeStr).IsValid()
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
depositType = types.DepositType(depositTypeStr)
|
||||
}
|
||||
|
||||
params := types.NewQueryDepositParams(page, limit, depositDenom, claimOwner, depositType)
|
||||
|
||||
bz, err := cliCtx.Codec.MarshalJSON(params)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetClaims)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rest.PostProcessResponse(w, cliCtx, res)
|
||||
}
|
||||
}
|
||||
|
||||
func queryModAccountsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
// Parse the query height
|
||||
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var name string
|
||||
|
||||
if x := r.URL.Query().Get(RestName); len(x) != 0 {
|
||||
name = strings.TrimSpace(x)
|
||||
}
|
||||
|
||||
params := types.NewQueryAccountParams(page, limit, name)
|
||||
|
||||
bz, err := cliCtx.Codec.MarshalJSON(params)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetModuleAccounts)
|
||||
res, height, err := cliCtx.QueryWithData(route, bz)
|
||||
cliCtx = cliCtx.WithHeight(height)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
rest.PostProcessResponse(w, cliCtx, res)
|
||||
}
|
||||
}
|
50
x/hvt/client/rest/rest.go
Normal file
50
x/hvt/client/rest/rest.go
Normal file
@ -0,0 +1,50 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
)
|
||||
|
||||
// REST variable names
|
||||
// nolint
|
||||
const (
|
||||
RestOwner = "owner"
|
||||
RestDenom = "deposit-denom"
|
||||
RestType = "deposit-type"
|
||||
RestName = "name"
|
||||
)
|
||||
|
||||
// RegisterRoutes registers harvest-related REST handlers to a router
|
||||
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||
registerQueryRoutes(cliCtx, r)
|
||||
registerTxRoutes(cliCtx, r)
|
||||
}
|
||||
|
||||
// PostCreateDepositReq defines the properties of a deposit create request's body
|
||||
type PostCreateDepositReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
From sdk.AccAddress `json:"from" yaml:"from"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// PostCreateWithdrawReq defines the properties of a deposit withdraw request's body
|
||||
type PostCreateWithdrawReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
From sdk.AccAddress `json:"from" yaml:"from"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// PostClaimReq defines the properties of a claim reward request's body
|
||||
type PostClaimReq struct {
|
||||
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
|
||||
From sdk.AccAddress `json:"from" yaml:"from"`
|
||||
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
Multiplier string `json:"multiplier" yaml:"multiplier"`
|
||||
}
|
85
x/hvt/client/rest/tx.go
Normal file
85
x/hvt/client/rest/tx.go
Normal file
@ -0,0 +1,85 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
|
||||
r.HandleFunc(fmt.Sprintf("/%s/deposit", types.ModuleName), postDepositHandlerFn(cliCtx)).Methods("POST")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/withdraw", types.ModuleName), postWithdrawHandlerFn(cliCtx)).Methods("POST")
|
||||
r.HandleFunc(fmt.Sprintf("/%s/claim", types.ModuleName), postClaimHandlerFn(cliCtx)).Methods("POST")
|
||||
}
|
||||
|
||||
func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode POST request body
|
||||
var req PostCreateDepositReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||
return
|
||||
}
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
msg := types.NewMsgDeposit(req.From, req.Amount, strings.ToLower(req.DepositType))
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||
}
|
||||
}
|
||||
|
||||
func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode POST request body
|
||||
var req PostCreateWithdrawReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||
return
|
||||
}
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
msg := types.NewMsgWithdraw(req.From, req.Amount, strings.ToLower(req.DepositType))
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||
}
|
||||
}
|
||||
|
||||
func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode POST request body
|
||||
var req PostClaimReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
|
||||
return
|
||||
}
|
||||
req.BaseReq = req.BaseReq.Sanitize()
|
||||
if !req.BaseReq.ValidateBasic(w) {
|
||||
return
|
||||
}
|
||||
|
||||
msg := types.NewMsgClaimReward(req.From, req.Receiver, req.DepositDenom, strings.ToLower(req.DepositType), req.Multiplier)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
|
||||
}
|
||||
}
|
65
x/hvt/genesis.go
Normal file
65
x/hvt/genesis.go
Normal file
@ -0,0 +1,65 @@
|
||||
package hvt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// InitGenesis initializes the store state from a genesis state.
|
||||
func InitGenesis(ctx sdk.Context, k Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) {
|
||||
if err := gs.Validate(); err != nil {
|
||||
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
|
||||
}
|
||||
|
||||
k.SetParams(ctx, gs.Params)
|
||||
|
||||
// only set the previous block time if it's different than default
|
||||
if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) {
|
||||
k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
|
||||
}
|
||||
|
||||
for _, pdt := range gs.PreviousDistributionTimes {
|
||||
if !pdt.PreviousDistributionTime.Equal(DefaultPreviousBlockTime) {
|
||||
k.SetPreviousDelegationDistribution(ctx, pdt.PreviousDistributionTime, pdt.Denom)
|
||||
}
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
LPModuleAcc := supplyKeeper.GetModuleAccount(ctx, LPAccount)
|
||||
if LPModuleAcc == nil {
|
||||
panic(fmt.Sprintf("%s module account has not been set", LPAccount))
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
DelegatorModuleAcc := supplyKeeper.GetModuleAccount(ctx, DelegatorAccount)
|
||||
if DelegatorModuleAcc == nil {
|
||||
panic(fmt.Sprintf("%s module account has not been set", DelegatorAccount))
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
DepositModuleAccount := supplyKeeper.GetModuleAccount(ctx, ModuleAccountName)
|
||||
if DepositModuleAccount == nil {
|
||||
panic(fmt.Sprintf("%s module account has not been set", DepositModuleAccount))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ExportGenesis export genesis state for harvest module
|
||||
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
|
||||
params := k.GetParams(ctx)
|
||||
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
|
||||
if !found {
|
||||
previousBlockTime = DefaultPreviousBlockTime
|
||||
}
|
||||
previousDistTimes := GenesisDistributionTimes{}
|
||||
for _, dds := range params.DelegatorDistributionSchedules {
|
||||
previousDistTime, found := k.GetPreviousDelegatorDistribution(ctx, dds.DistributionSchedule.DepositDenom)
|
||||
if found {
|
||||
previousDistTimes = append(previousDistTimes, GenesisDistributionTime{PreviousDistributionTime: previousDistTime, Denom: dds.DistributionSchedule.DepositDenom})
|
||||
}
|
||||
}
|
||||
return NewGenesisState(params, previousBlockTime, previousDistTimes)
|
||||
}
|
82
x/hvt/handler.go
Normal file
82
x/hvt/handler.go
Normal file
@ -0,0 +1,82 @@
|
||||
package hvt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/keeper"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// NewHandler creates an sdk.Handler for harvest messages
|
||||
func NewHandler(k Keeper) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
|
||||
ctx = ctx.WithEventManager(sdk.NewEventManager())
|
||||
switch msg := msg.(type) {
|
||||
case types.MsgClaimReward:
|
||||
return handleMsgClaimReward(ctx, k, msg)
|
||||
case types.MsgDeposit:
|
||||
return handleMsgDeposit(ctx, k, msg)
|
||||
case types.MsgWithdraw:
|
||||
return handleMsgWithdraw(ctx, k, msg)
|
||||
default:
|
||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleMsgClaimReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimReward) (*sdk.Result, error) {
|
||||
err := k.ClaimReward(ctx, msg.Sender, msg.Receiver, msg.DepositDenom, types.DepositType(strings.ToLower(msg.DepositType)), types.MultiplierName(strings.ToLower(msg.MultiplierName)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
|
||||
),
|
||||
)
|
||||
return &sdk.Result{
|
||||
Events: ctx.EventManager().Events(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleMsgDeposit(ctx sdk.Context, k keeper.Keeper, msg types.MsgDeposit) (*sdk.Result, error) {
|
||||
err := k.Deposit(ctx, msg.Depositor, msg.Amount, types.DepositType(strings.ToLower(msg.DepositType)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
|
||||
),
|
||||
)
|
||||
return &sdk.Result{
|
||||
Events: ctx.EventManager().Events(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw) (*sdk.Result, error) {
|
||||
err := k.Withdraw(ctx, msg.Depositor, msg.Amount, types.DepositType(strings.ToLower(msg.DepositType)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
|
||||
),
|
||||
)
|
||||
return &sdk.Result{
|
||||
Events: ctx.EventManager().Events(),
|
||||
}, nil
|
||||
}
|
148
x/hvt/keeper/claim.go
Normal file
148
x/hvt/keeper/claim.go
Normal file
@ -0,0 +1,148 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||
)
|
||||
|
||||
const (
|
||||
// BeginningOfMonth harvest rewards that are claimed after the 15th at 14:00UTC of the month always vest on the first of the month
|
||||
BeginningOfMonth = 1
|
||||
// MidMonth harvest rewards that are claimed before the 15th at 14:00UTC of the month always vest on the 15 of the month
|
||||
MidMonth = 15
|
||||
// PaymentHour harvest rewards always vest at 14:00UTC
|
||||
PaymentHour = 14
|
||||
)
|
||||
|
||||
// ClaimReward sends the reward amount to the reward owner and deletes the claim from the store
|
||||
func (k Keeper) ClaimReward(ctx sdk.Context, claimHolder sdk.AccAddress, receiver sdk.AccAddress, depositDenom string, depositType types.DepositType, multiplier types.MultiplierName) error {
|
||||
|
||||
claim, found := k.GetClaim(ctx, claimHolder, depositDenom, depositType)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrClaimNotFound, "no %s %s claim found for %s", depositDenom, depositType, claimHolder)
|
||||
}
|
||||
|
||||
err := k.validateSenderReceiver(ctx, claimHolder, receiver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch depositType {
|
||||
case types.LP:
|
||||
err = k.claimLPReward(ctx, claim, receiver, multiplier)
|
||||
case types.Stake:
|
||||
err = k.claimDelegatorReward(ctx, claim, receiver, multiplier)
|
||||
default:
|
||||
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeClaimHarvestReward,
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, claim.Amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyClaimHolder, claimHolder.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositDenom, depositDenom),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
|
||||
sdk.NewAttribute(types.AttributeKeyClaimMultiplier, string(multiplier)),
|
||||
),
|
||||
)
|
||||
k.DeleteClaim(ctx, claim)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPeriodLength returns the length of the period based on the input blocktime and multiplier
|
||||
// note that pay dates are always the 1st or 15th of the month at 14:00UTC.
|
||||
func (k Keeper) GetPeriodLength(ctx sdk.Context, multiplier types.Multiplier) (int64, error) {
|
||||
|
||||
switch multiplier.Name {
|
||||
case types.Small:
|
||||
return 0, nil
|
||||
case types.Medium, types.Large:
|
||||
currentDay := ctx.BlockTime().Day()
|
||||
payDay := BeginningOfMonth
|
||||
monthOffset := int64(1)
|
||||
if currentDay < MidMonth || (currentDay == MidMonth && ctx.BlockTime().Hour() < PaymentHour) {
|
||||
payDay = MidMonth
|
||||
monthOffset = int64(0)
|
||||
}
|
||||
periodEndDate := time.Date(ctx.BlockTime().Year(), ctx.BlockTime().Month(), payDay, PaymentHour, 0, 0, 0, time.UTC).AddDate(0, int(multiplier.MonthsLockup+monthOffset), 0)
|
||||
return periodEndDate.Unix() - ctx.BlockTime().Unix(), nil
|
||||
}
|
||||
return 0, types.ErrInvalidMultiplier
|
||||
}
|
||||
|
||||
func (k Keeper) claimLPReward(ctx sdk.Context, claim types.Claim, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
|
||||
lps, found := k.GetLPSchedule(ctx, claim.DepositDenom)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrLPScheduleNotFound, claim.DepositDenom)
|
||||
}
|
||||
multiplier, found := lps.GetMultiplier(multiplierName)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
|
||||
}
|
||||
if ctx.BlockTime().After(lps.ClaimEnd) {
|
||||
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), lps.ClaimEnd)
|
||||
}
|
||||
rewardAmount := sdk.NewDecFromInt(claim.Amount.Amount).Mul(multiplier.Factor).RoundInt()
|
||||
if rewardAmount.IsZero() {
|
||||
return types.ErrZeroClaim
|
||||
}
|
||||
rewardCoin := sdk.NewCoin(claim.Amount.Denom, rewardAmount)
|
||||
length, err := k.GetPeriodLength(ctx, multiplier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return k.SendTimeLockedCoinsToAccount(ctx, types.LPAccount, receiver, sdk.NewCoins(rewardCoin), length)
|
||||
}
|
||||
|
||||
func (k Keeper) claimDelegatorReward(ctx sdk.Context, claim types.Claim, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
|
||||
dss, found := k.GetDelegatorSchedule(ctx, claim.DepositDenom)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrLPScheduleNotFound, claim.DepositDenom)
|
||||
}
|
||||
multiplier, found := dss.DistributionSchedule.GetMultiplier(multiplierName)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
|
||||
}
|
||||
if ctx.BlockTime().After(dss.DistributionSchedule.ClaimEnd) {
|
||||
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), dss.DistributionSchedule.ClaimEnd)
|
||||
}
|
||||
rewardAmount := sdk.NewDecFromInt(claim.Amount.Amount).Mul(multiplier.Factor).RoundInt()
|
||||
if rewardAmount.IsZero() {
|
||||
return types.ErrZeroClaim
|
||||
}
|
||||
rewardCoin := sdk.NewCoin(claim.Amount.Denom, rewardAmount)
|
||||
|
||||
length, err := k.GetPeriodLength(ctx, multiplier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return k.SendTimeLockedCoinsToAccount(ctx, types.DelegatorAccount, receiver, sdk.NewCoins(rewardCoin), length)
|
||||
}
|
||||
|
||||
func (k Keeper) validateSenderReceiver(ctx sdk.Context, sender, receiver sdk.AccAddress) error {
|
||||
senderAcc := k.accountKeeper.GetAccount(ctx, sender)
|
||||
if senderAcc == nil {
|
||||
return sdkerrors.Wrapf(types.ErrAccountNotFound, sender.String())
|
||||
}
|
||||
switch senderAcc.(type) {
|
||||
case *validatorvesting.ValidatorVestingAccount:
|
||||
if sender.Equals(receiver) {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", senderAcc)
|
||||
}
|
||||
default:
|
||||
if !sender.Equals(receiver) {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidReceiver, "%s", sender)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
470
x/hvt/keeper/claim_test.go
Normal file
470
x/hvt/keeper/claim_test.go
Normal file
@ -0,0 +1,470 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestClaim() {
|
||||
type args struct {
|
||||
claimOwner sdk.AccAddress
|
||||
receiver sdk.AccAddress
|
||||
denom string
|
||||
depositType types.DepositType
|
||||
multiplier types.MultiplierName
|
||||
blockTime time.Time
|
||||
createClaim bool
|
||||
claimAmount sdk.Coin
|
||||
validatorVesting bool
|
||||
expectedAccountBalance sdk.Coins
|
||||
expectedModAccountBalance sdk.Coins
|
||||
expectedVestingAccount bool
|
||||
expectedVestingLength int64
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type claimTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []claimTest{
|
||||
{
|
||||
"valid liquid claim",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(33)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(967))),
|
||||
expectedVestingAccount: false,
|
||||
expectedVestingLength: 0,
|
||||
multiplier: types.Small,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid liquid delegator claim",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.Stake,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(33)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(967))),
|
||||
expectedVestingAccount: false,
|
||||
expectedVestingLength: 0,
|
||||
multiplier: types.Small,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid medium vesting claim",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(50)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(950))),
|
||||
expectedVestingAccount: true,
|
||||
expectedVestingLength: 16848000,
|
||||
multiplier: types.Medium,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid large vesting claim",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
|
||||
expectedVestingAccount: true,
|
||||
expectedVestingLength: 64281600,
|
||||
multiplier: types.Large,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid validator vesting",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test2"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
|
||||
expectedVestingAccount: true,
|
||||
expectedVestingLength: 64281600,
|
||||
multiplier: types.Large,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid validator vesting",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
|
||||
expectedVestingAccount: true,
|
||||
expectedVestingLength: 64281600,
|
||||
multiplier: types.Large,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "receiver account type not supported",
|
||||
},
|
||||
},
|
||||
{
|
||||
"claim not found",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: false,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
expectedVestingAccount: false,
|
||||
expectedVestingLength: 0,
|
||||
multiplier: types.Small,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "claim not found",
|
||||
},
|
||||
},
|
||||
{
|
||||
"claim expired",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2022, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
expectedVestingAccount: false,
|
||||
expectedVestingLength: 0,
|
||||
multiplier: types.Small,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "claim period expired",
|
||||
},
|
||||
},
|
||||
{
|
||||
"different receiver address",
|
||||
args{
|
||||
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test2"))),
|
||||
denom: "bnb",
|
||||
depositType: types.LP,
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
createClaim: true,
|
||||
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
|
||||
validatorVesting: false,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
|
||||
expectedVestingAccount: true,
|
||||
expectedVestingLength: 64281600,
|
||||
multiplier: types.Large,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "receiver account must match sender account",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create new app with one funded account
|
||||
config := sdk.GetConfig()
|
||||
app.SetBech32AddressPrefixes(config)
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
|
||||
authGS := app.NewAuthGenState(
|
||||
[]sdk.AccAddress{tc.args.claimOwner, tc.args.receiver},
|
||||
[]sdk.Coins{
|
||||
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
})
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
if tc.args.validatorVesting {
|
||||
ak := tApp.GetAccountKeeper()
|
||||
acc := ak.GetAccount(ctx, tc.args.claimOwner)
|
||||
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
|
||||
bva, err := vesting.NewBaseVestingAccount(
|
||||
bacc,
|
||||
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(20))), time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix()+100)
|
||||
suite.Require().NoError(err)
|
||||
vva := validatorvesting.NewValidatorVestingAccountRaw(
|
||||
bva,
|
||||
time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix(),
|
||||
vesting.Periods{
|
||||
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}},
|
||||
sdk.ConsAddress(crypto.AddressHash([]byte("test"))),
|
||||
sdk.AccAddress{},
|
||||
95,
|
||||
)
|
||||
err = vva.Validate()
|
||||
suite.Require().NoError(err)
|
||||
ak.SetAccount(ctx, vva)
|
||||
}
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.LPAccount, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
|
||||
supplyKeeper.MintCoins(ctx, types.DelegatorAccount, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
if tc.args.createClaim {
|
||||
claim := types.NewClaim(tc.args.claimOwner, tc.args.denom, tc.args.claimAmount, tc.args.depositType)
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetClaim(suite.ctx, claim) })
|
||||
}
|
||||
|
||||
err := suite.keeper.ClaimReward(suite.ctx, tc.args.claimOwner, tc.args.receiver, tc.args.denom, tc.args.depositType, tc.args.multiplier)
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
acc := suite.getAccount(tc.args.receiver)
|
||||
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
|
||||
mAcc := suite.getModuleAccount(types.LPAccount)
|
||||
if tc.args.depositType == types.Stake {
|
||||
mAcc = suite.getModuleAccount(types.DelegatorAccount)
|
||||
}
|
||||
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
|
||||
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
|
||||
if tc.args.expectedVestingAccount {
|
||||
suite.Require().True(ok)
|
||||
suite.Require().Equal(tc.args.expectedVestingLength, vacc.VestingPeriods[0].Length)
|
||||
} else {
|
||||
suite.Require().False(ok)
|
||||
}
|
||||
_, f := suite.keeper.GetClaim(ctx, tc.args.claimOwner, tc.args.denom, tc.args.depositType)
|
||||
suite.Require().False(f)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetPeriodLength() {
|
||||
type args struct {
|
||||
blockTime time.Time
|
||||
multiplier types.Multiplier
|
||||
expectedLength int64
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type periodTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []periodTest{
|
||||
{
|
||||
name: "first half of month",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2021, 5, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "first half of month long lockup",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2022, 11, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "second half of month",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2021, 7, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "second half of month long lockup",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Large, 24, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2023, 1, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "end of feb",
|
||||
args: args{
|
||||
blockTime: time.Date(2021, 2, 28, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2021, 9, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2021, 2, 28, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "leap year",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2020, 9, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "leap year long lockup",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Large, 24, sdk.MustNewDecFromStr("1")),
|
||||
expectedLength: time.Date(2022, 3, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exactly half of month",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2021, 7, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just before half of month",
|
||||
args: args{
|
||||
blockTime: time.Date(2020, 12, 15, 13, 59, 59, 0, time.UTC),
|
||||
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
|
||||
expectedLength: time.Date(2021, 6, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 15, 13, 59, 59, 0, time.UTC).Unix(),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
ctx := suite.ctx.WithBlockTime(tc.args.blockTime)
|
||||
length, err := suite.keeper.GetPeriodLength(ctx, tc.args.multiplier)
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equal(tc.args.expectedLength, length)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
136
x/hvt/keeper/deposit.go
Normal file
136
x/hvt/keeper/deposit.go
Normal file
@ -0,0 +1,136 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// Deposit deposit
|
||||
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositType types.DepositType) error {
|
||||
|
||||
err := k.ValidateDeposit(ctx, amount, depositType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch depositType {
|
||||
case types.LP:
|
||||
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleAccountName, sdk.NewCoins(amount))
|
||||
default:
|
||||
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deposit, found := k.GetDeposit(ctx, depositor, amount.Denom, depositType)
|
||||
if !found {
|
||||
deposit = types.NewDeposit(depositor, amount, depositType)
|
||||
} else {
|
||||
deposit.Amount = deposit.Amount.Add(amount)
|
||||
}
|
||||
|
||||
k.SetDeposit(ctx, deposit)
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHarvestDeposit,
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, deposit.Depositor.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositDenom, deposit.Amount.Denom),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
|
||||
),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDeposit validates a deposit
|
||||
func (k Keeper) ValidateDeposit(ctx sdk.Context, amount sdk.Coin, depositType types.DepositType) error {
|
||||
var err error
|
||||
switch depositType {
|
||||
case types.LP:
|
||||
err = k.ValidateLPDeposit(ctx, amount, depositType)
|
||||
default:
|
||||
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateLPDeposit validates that a liquidity provider deposit
|
||||
func (k Keeper) ValidateLPDeposit(ctx sdk.Context, amount sdk.Coin, depositType types.DepositType) error {
|
||||
params := k.GetParams(ctx)
|
||||
for _, lps := range params.LiquidityProviderSchedules {
|
||||
if lps.DepositDenom == amount.Denom {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return sdkerrors.Wrapf(types.ErrInvalidDepositDenom, "liquidity provider denom %s not found", amount.Denom)
|
||||
}
|
||||
|
||||
// Withdraw returns some or all of a deposit back to original depositor
|
||||
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositType types.DepositType) error {
|
||||
deposit, found := k.GetDeposit(ctx, depositor, amount.Denom, depositType)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no %s %s deposit found for %s", amount.Denom, depositType, depositor)
|
||||
}
|
||||
if !deposit.Amount.IsGTE(amount) {
|
||||
return sdkerrors.Wrapf(types.ErrInvaliWithdrawAmount, "%s>%s", amount, deposit.Amount)
|
||||
}
|
||||
|
||||
var err error
|
||||
switch depositType {
|
||||
case types.LP:
|
||||
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, sdk.NewCoins(amount))
|
||||
default:
|
||||
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHarvestWithdrawal,
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositDenom, amount.Denom),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
|
||||
),
|
||||
)
|
||||
|
||||
if deposit.Amount.IsEqual(amount) {
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeDeleteHarvestDeposit,
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositDenom, amount.Denom),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
|
||||
),
|
||||
)
|
||||
k.DeleteDeposit(ctx, deposit)
|
||||
return nil
|
||||
}
|
||||
|
||||
deposit.Amount = deposit.Amount.Sub(amount)
|
||||
k.SetDeposit(ctx, deposit)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTotalDeposited returns the total amount deposited for the input deposit type and deposit denom
|
||||
func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositType types.DepositType, depositDenom string) (total sdk.Int) {
|
||||
|
||||
var macc supplyExported.ModuleAccountI
|
||||
switch depositType {
|
||||
case types.LP:
|
||||
macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
||||
}
|
||||
return macc.GetCoins().AmountOf(depositDenom)
|
||||
}
|
332
x/hvt/keeper/deposit_test.go
Normal file
332
x/hvt/keeper/deposit_test.go
Normal file
@ -0,0 +1,332 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestDeposit() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
amount sdk.Coin
|
||||
depositType types.DepositType
|
||||
numberDeposits int
|
||||
expectedAccountBalance sdk.Coins
|
||||
expectedModAccountBalance sdk.Coins
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type depositTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []depositTest{
|
||||
{
|
||||
"valid",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(100)),
|
||||
depositType: types.LP,
|
||||
numberDeposits: 1,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid multi deposit",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(100)),
|
||||
depositType: types.LP,
|
||||
numberDeposits: 2,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(800)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid deposit type",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(100)),
|
||||
depositType: types.Stake,
|
||||
numberDeposits: 1,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "invalid deposit type",
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid deposit denom",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
amount: sdk.NewCoin("btcb", sdk.NewInt(100)),
|
||||
depositType: types.LP,
|
||||
numberDeposits: 1,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "invalid deposit denom",
|
||||
},
|
||||
},
|
||||
{
|
||||
"insufficient funds",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000)),
|
||||
depositType: types.LP,
|
||||
numberDeposits: 1,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "insufficient funds",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create new app with one funded account
|
||||
config := sdk.GetConfig()
|
||||
app.SetBech32AddressPrefixes(config)
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
// run the test
|
||||
var err error
|
||||
for i := 0; i < tc.args.numberDeposits; i++ {
|
||||
err = suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.amount, tc.args.depositType)
|
||||
}
|
||||
|
||||
// verify results
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
acc := suite.getAccount(tc.args.depositor)
|
||||
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
|
||||
mAcc := suite.getModuleAccount(types.ModuleAccountName)
|
||||
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
|
||||
_, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor, tc.args.amount.Denom, tc.args.depositType)
|
||||
suite.Require().True(f)
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestWithdraw() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
depositAmount sdk.Coin
|
||||
withdrawAmount sdk.Coin
|
||||
depositType types.DepositType
|
||||
withdrawType types.DepositType
|
||||
createDeposit bool
|
||||
expectedAccountBalance sdk.Coins
|
||||
expectedModAccountBalance sdk.Coins
|
||||
depositExists bool
|
||||
finalDepositAmount sdk.Coin
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type withdrawTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []withdrawTest{
|
||||
{
|
||||
"valid partial withdraw",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(100)),
|
||||
depositType: types.LP,
|
||||
withdrawType: types.LP,
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
depositExists: true,
|
||||
finalDepositAmount: sdk.NewCoin("bnb", sdk.NewInt(100)),
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid full withdraw",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
depositType: types.LP,
|
||||
withdrawType: types.LP,
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.Coins(nil),
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coin{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"deposit not found invalid denom",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
withdrawAmount: sdk.NewCoin("btcb", sdk.NewInt(200)),
|
||||
depositType: types.LP,
|
||||
withdrawType: types.LP,
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coin{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "deposit not found",
|
||||
},
|
||||
},
|
||||
{
|
||||
"deposit not found invalid deposit type",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
depositType: types.LP,
|
||||
withdrawType: types.Stake,
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coin{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "deposit not found",
|
||||
},
|
||||
},
|
||||
{
|
||||
"withdraw exceeds deposit",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
|
||||
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(300)),
|
||||
depositType: types.LP,
|
||||
withdrawType: types.LP,
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coin{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "withdrawal amount exceeds deposit amount",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create new app with one funded account
|
||||
config := sdk.GetConfig()
|
||||
app.SetBech32AddressPrefixes(config)
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
if tc.args.createDeposit {
|
||||
err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount, tc.args.depositType)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount, tc.args.withdrawType)
|
||||
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
acc := suite.getAccount(tc.args.depositor)
|
||||
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
|
||||
mAcc := suite.getModuleAccount(types.ModuleAccountName)
|
||||
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
|
||||
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor, tc.args.depositAmount.Denom, tc.args.depositType)
|
||||
if tc.args.depositExists {
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
|
||||
} else {
|
||||
suite.Require().False(f)
|
||||
}
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
184
x/hvt/keeper/keeper.go
Normal file
184
x/hvt/keeper/keeper.go
Normal file
@ -0,0 +1,184 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/params/subspace"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// Keeper keeper for the harvest module
|
||||
type Keeper struct {
|
||||
key sdk.StoreKey
|
||||
cdc *codec.Codec
|
||||
paramSubspace subspace.Subspace
|
||||
accountKeeper types.AccountKeeper
|
||||
supplyKeeper types.SupplyKeeper
|
||||
stakingKeeper types.StakingKeeper
|
||||
}
|
||||
|
||||
// NewKeeper creates a new keeper
|
||||
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper) Keeper {
|
||||
if !paramstore.HasKeyTable() {
|
||||
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
|
||||
}
|
||||
|
||||
return Keeper{
|
||||
key: key,
|
||||
cdc: cdc,
|
||||
paramSubspace: paramstore,
|
||||
accountKeeper: ak,
|
||||
supplyKeeper: sk,
|
||||
stakingKeeper: stk,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPreviousBlockTime get the blocktime for the previous block
|
||||
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
|
||||
b := store.Get([]byte{})
|
||||
if b == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
k.cdc.MustUnmarshalBinaryBare(b, &blockTime)
|
||||
return blockTime, true
|
||||
}
|
||||
|
||||
// SetPreviousBlockTime set the time of the previous block
|
||||
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
|
||||
store.Set([]byte{}, k.cdc.MustMarshalBinaryBare(blockTime))
|
||||
}
|
||||
|
||||
// GetPreviousDelegatorDistribution get the time of the previous delegator distribution
|
||||
func (k Keeper) GetPreviousDelegatorDistribution(ctx sdk.Context, denom string) (distTime time.Time, found bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDelegationDistributionKey)
|
||||
bz := store.Get([]byte(denom))
|
||||
if bz == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &distTime)
|
||||
return distTime, true
|
||||
}
|
||||
|
||||
// SetPreviousDelegationDistribution set the time of the previous delegator distribution
|
||||
func (k Keeper) SetPreviousDelegationDistribution(ctx sdk.Context, distTime time.Time, denom string) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDelegationDistributionKey)
|
||||
store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(distTime))
|
||||
}
|
||||
|
||||
// GetDeposit returns a deposit from the store for a particular depositor address, deposit denom, and deposit type
|
||||
func (k Keeper) GetDeposit(ctx sdk.Context, depositor sdk.AccAddress, denom string, depositType types.DepositType) (types.Deposit, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
|
||||
bz := store.Get(types.DepositKey(depositType, denom, depositor))
|
||||
if bz == nil {
|
||||
return types.Deposit{}, false
|
||||
}
|
||||
var deposit types.Deposit
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &deposit)
|
||||
return deposit, true
|
||||
}
|
||||
|
||||
// SetDeposit sets the input deposit in the store, prefixed by the deposit type, deposit denom, and depositor address, in that order
|
||||
func (k Keeper) SetDeposit(ctx sdk.Context, deposit types.Deposit) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(deposit)
|
||||
store.Set(types.DepositKey(deposit.Type, deposit.Amount.Denom, deposit.Depositor), bz)
|
||||
}
|
||||
|
||||
// DeleteDeposit deletes a deposit from the store
|
||||
func (k Keeper) DeleteDeposit(ctx sdk.Context, deposit types.Deposit) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
|
||||
store.Delete(types.DepositKey(deposit.Type, deposit.Amount.Denom, deposit.Depositor))
|
||||
}
|
||||
|
||||
// IterateDeposits iterates over all deposit objects in the store and performs a callback function
|
||||
func (k Keeper) IterateDeposits(ctx sdk.Context, cb func(deposit types.Deposit) (stop bool)) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
|
||||
iterator := sdk.KVStorePrefixIterator(store, []byte{})
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var deposit types.Deposit
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &deposit)
|
||||
if cb(deposit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IterateDepositsByTypeAndDenom iterates over all deposit objects in the store with the matching deposit type and deposit denom and performs a callback function
|
||||
func (k Keeper) IterateDepositsByTypeAndDenom(ctx sdk.Context, depositType types.DepositType, depositDenom string, cb func(deposit types.Deposit) (stop bool)) {
|
||||
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
|
||||
iterator := sdk.KVStorePrefixIterator(store, types.DepositTypeIteratorKey(depositType, depositDenom))
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var deposit types.Deposit
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &deposit)
|
||||
if cb(deposit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetClaim returns a claim from the store for a particular claim owner, deposit denom, and deposit type
|
||||
func (k Keeper) GetClaim(ctx sdk.Context, owner sdk.AccAddress, depositDenom string, depositType types.DepositType) (types.Claim, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
|
||||
bz := store.Get(types.ClaimKey(depositType, depositDenom, owner))
|
||||
if bz == nil {
|
||||
return types.Claim{}, false
|
||||
}
|
||||
var claim types.Claim
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &claim)
|
||||
return claim, true
|
||||
}
|
||||
|
||||
// SetClaim stores the input claim in the store, prefixed by the deposit type, deposit denom, and owner address, in that order
|
||||
func (k Keeper) SetClaim(ctx sdk.Context, claim types.Claim) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(claim)
|
||||
store.Set(types.ClaimKey(claim.Type, claim.DepositDenom, claim.Owner), bz)
|
||||
}
|
||||
|
||||
// DeleteClaim deletes a claim from the store
|
||||
func (k Keeper) DeleteClaim(ctx sdk.Context, claim types.Claim) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
|
||||
store.Delete(types.ClaimKey(claim.Type, claim.DepositDenom, claim.Owner))
|
||||
}
|
||||
|
||||
// IterateClaims iterates over all claim objects in the store and performs a callback function
|
||||
func (k Keeper) IterateClaims(ctx sdk.Context, cb func(claim types.Claim) (stop bool)) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
|
||||
iterator := sdk.KVStorePrefixIterator(store, []byte{})
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var claim types.Claim
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &claim)
|
||||
if cb(claim) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IterateClaimsByTypeAndDenom iterates over all claim objects in the store with the matching deposit type and deposit denom and performs a callback function
|
||||
func (k Keeper) IterateClaimsByTypeAndDenom(ctx sdk.Context, depositType types.DepositType, depositDenom string, cb func(claim types.Claim) (stop bool)) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
|
||||
iterator := sdk.KVStorePrefixIterator(store, types.DepositTypeIteratorKey(depositType, depositDenom))
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var claim types.Claim
|
||||
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &claim)
|
||||
if cb(claim) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BondDenom returns the bond denom from the staking keeper
|
||||
func (k Keeper) BondDenom(ctx sdk.Context) string {
|
||||
return k.stakingKeeper.BondDenom(ctx)
|
||||
}
|
165
x/hvt/keeper/keeper_test.go
Normal file
165
x/hvt/keeper/keeper_test.go
Normal file
@ -0,0 +1,165 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
|
||||
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/hvt/keeper"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// Test suite used for all keeper tests
|
||||
type KeeperTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
keeper keeper.Keeper
|
||||
app app.TestApp
|
||||
ctx sdk.Context
|
||||
addrs []sdk.AccAddress
|
||||
}
|
||||
|
||||
// The default state used by each test
|
||||
func (suite *KeeperTestSuite) SetupTest() {
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
tApp.InitializeFromGenesisStates()
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(1)
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
suite.addrs = addrs
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetSetPreviousBlockTime() {
|
||||
now := tmtime.Now()
|
||||
|
||||
_, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
|
||||
suite.Require().False(f)
|
||||
|
||||
suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, now) })
|
||||
|
||||
pbt, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
|
||||
suite.True(f)
|
||||
suite.Equal(now, pbt)
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetSetPreviousDelegatorDistribution() {
|
||||
now := tmtime.Now()
|
||||
|
||||
_, f := suite.keeper.GetPreviousDelegatorDistribution(suite.ctx, suite.keeper.BondDenom(suite.ctx))
|
||||
suite.Require().False(f)
|
||||
|
||||
suite.NotPanics(func() {
|
||||
suite.keeper.SetPreviousDelegationDistribution(suite.ctx, now, suite.keeper.BondDenom(suite.ctx))
|
||||
})
|
||||
|
||||
pdt, f := suite.keeper.GetPreviousDelegatorDistribution(suite.ctx, suite.keeper.BondDenom(suite.ctx))
|
||||
suite.True(f)
|
||||
suite.Equal(now, pdt)
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
|
||||
dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoin("bnb", sdk.NewInt(100)), "lp")
|
||||
|
||||
_, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
|
||||
suite.Require().False(f)
|
||||
|
||||
suite.keeper.SetDeposit(suite.ctx, dep)
|
||||
|
||||
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(dep, testDeposit)
|
||||
|
||||
suite.Require().NotPanics(func() { suite.keeper.DeleteDeposit(suite.ctx, dep) })
|
||||
|
||||
_, f = suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
|
||||
suite.Require().False(f)
|
||||
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestIterateDeposits() {
|
||||
for i := 0; i < 5; i++ {
|
||||
dep := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("bnb", sdk.NewInt(100)), "lp")
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, dep) })
|
||||
}
|
||||
var deposits []types.Deposit
|
||||
suite.keeper.IterateDeposits(suite.ctx, func(d types.Deposit) bool {
|
||||
deposits = append(deposits, d)
|
||||
return false
|
||||
})
|
||||
suite.Require().Equal(5, len(deposits))
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestIterateDepositsByTypeAndDenom() {
|
||||
for i := 0; i < 5; i++ {
|
||||
depA := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("bnb", sdk.NewInt(100)), "lp")
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, depA) })
|
||||
depB := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("bnb", sdk.NewInt(100)), "gov")
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, depB) })
|
||||
depC := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("btcb", sdk.NewInt(100)), "lp")
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, depC) })
|
||||
}
|
||||
var bnbLPDeposits []types.Deposit
|
||||
suite.keeper.IterateDepositsByTypeAndDenom(suite.ctx, "lp", "bnb", func(d types.Deposit) bool {
|
||||
bnbLPDeposits = append(bnbLPDeposits, d)
|
||||
return false
|
||||
})
|
||||
suite.Require().Equal(5, len(bnbLPDeposits))
|
||||
var bnbGovDeposits []types.Deposit
|
||||
suite.keeper.IterateDepositsByTypeAndDenom(suite.ctx, "gov", "bnb", func(d types.Deposit) bool {
|
||||
bnbGovDeposits = append(bnbGovDeposits, d)
|
||||
return false
|
||||
})
|
||||
suite.Require().Equal(5, len(bnbGovDeposits))
|
||||
var btcbLPDeposits []types.Deposit
|
||||
suite.keeper.IterateDepositsByTypeAndDenom(suite.ctx, "lp", "btcb", func(d types.Deposit) bool {
|
||||
btcbLPDeposits = append(btcbLPDeposits, d)
|
||||
return false
|
||||
})
|
||||
suite.Require().Equal(5, len(btcbLPDeposits))
|
||||
var deposits []types.Deposit
|
||||
suite.keeper.IterateDeposits(suite.ctx, func(d types.Deposit) bool {
|
||||
deposits = append(deposits, d)
|
||||
return false
|
||||
})
|
||||
suite.Require().Equal(15, len(deposits))
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
|
||||
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "lp")
|
||||
_, f := suite.keeper.GetClaim(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
|
||||
suite.Require().False(f)
|
||||
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetClaim(suite.ctx, claim) })
|
||||
testClaim, f := suite.keeper.GetClaim(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(claim, testClaim)
|
||||
|
||||
suite.Require().NotPanics(func() { suite.keeper.DeleteClaim(suite.ctx, claim) })
|
||||
_, f = suite.keeper.GetClaim(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
|
||||
suite.Require().False(f)
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
|
||||
ak := suite.app.GetAccountKeeper()
|
||||
return ak.GetAccount(suite.ctx, addr)
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI {
|
||||
sk := suite.app.GetSupplyKeeper()
|
||||
return sk.GetModuleAccount(suite.ctx, name)
|
||||
}
|
||||
|
||||
func TestKeeperTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(KeeperTestSuite))
|
||||
}
|
39
x/hvt/keeper/params.go
Normal file
39
x/hvt/keeper/params.go
Normal file
@ -0,0 +1,39 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// GetParams returns the params from the store
|
||||
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
|
||||
var p types.Params
|
||||
k.paramSubspace.GetParamSet(ctx, &p)
|
||||
return p
|
||||
}
|
||||
|
||||
// SetParams sets params on the store
|
||||
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
|
||||
k.paramSubspace.SetParamSet(ctx, ¶ms)
|
||||
}
|
||||
|
||||
func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.DistributionSchedule, bool) {
|
||||
params := k.GetParams(ctx)
|
||||
for _, lps := range params.LiquidityProviderSchedules {
|
||||
if lps.DepositDenom == denom {
|
||||
return lps, true
|
||||
}
|
||||
}
|
||||
return types.DistributionSchedule{}, false
|
||||
}
|
||||
|
||||
func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.DelegatorDistributionSchedule, bool) {
|
||||
params := k.GetParams(ctx)
|
||||
for _, dds := range params.DelegatorDistributionSchedules {
|
||||
if dds.DistributionSchedule.DepositDenom == denom {
|
||||
return dds, true
|
||||
}
|
||||
}
|
||||
return types.DelegatorDistributionSchedule{}, false
|
||||
}
|
280
x/hvt/keeper/querier.go
Normal file
280
x/hvt/keeper/querier.go
Normal file
@ -0,0 +1,280 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// NewQuerier is the module level router for state queries
|
||||
func NewQuerier(k Keeper) sdk.Querier {
|
||||
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
|
||||
switch path[0] {
|
||||
case types.QueryGetParams:
|
||||
return queryGetParams(ctx, req, k)
|
||||
case types.QueryGetModuleAccounts:
|
||||
return queryGetModAccounts(ctx, req, k)
|
||||
case types.QueryGetDeposits:
|
||||
return queryGetDeposits(ctx, req, k)
|
||||
case types.QueryGetClaims:
|
||||
return queryGetClaims(ctx, req, k)
|
||||
default:
|
||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
// Get params
|
||||
params := k.GetParams(ctx)
|
||||
|
||||
// Encode results
|
||||
bz, err := codec.MarshalJSONIndent(k.cdc, params)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
return bz, nil
|
||||
}
|
||||
|
||||
func queryGetModAccounts(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
|
||||
var params types.QueryAccountParams
|
||||
err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
var accs []supplyexported.ModuleAccountI
|
||||
if len(params.Name) > 0 {
|
||||
acc := k.supplyKeeper.GetModuleAccount(ctx, types.LPAccount)
|
||||
accs = append(accs, acc)
|
||||
} else {
|
||||
acc := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
||||
accs = append(accs, acc)
|
||||
acc = k.supplyKeeper.GetModuleAccount(ctx, types.LPAccount)
|
||||
accs = append(accs, acc)
|
||||
acc = k.supplyKeeper.GetModuleAccount(ctx, types.DelegatorAccount)
|
||||
accs = append(accs, acc)
|
||||
}
|
||||
|
||||
bz, err := codec.MarshalJSONIndent(k.cdc, accs)
|
||||
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
|
||||
return bz, nil
|
||||
}
|
||||
|
||||
func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
|
||||
var params types.QueryDepositParams
|
||||
err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
depositDenom := false
|
||||
owner := false
|
||||
depositType := false
|
||||
|
||||
if len(params.DepositDenom) > 0 {
|
||||
depositDenom = true
|
||||
}
|
||||
if len(params.Owner) > 0 {
|
||||
owner = true
|
||||
}
|
||||
if len(params.DepositType) > 0 {
|
||||
depositType = true
|
||||
}
|
||||
|
||||
var deposits []types.Deposit
|
||||
if depositDenom && owner && depositType {
|
||||
deposit, found := k.GetDeposit(ctx, params.Owner, params.DepositDenom, params.DepositType)
|
||||
if found {
|
||||
deposits = append(deposits, deposit)
|
||||
}
|
||||
} else if depositDenom && owner {
|
||||
for _, dt := range types.DepositTypesDepositQuery {
|
||||
deposit, found := k.GetDeposit(ctx, params.Owner, params.DepositDenom, dt)
|
||||
if found {
|
||||
deposits = append(deposits, deposit)
|
||||
}
|
||||
}
|
||||
} else if depositDenom && depositType {
|
||||
k.IterateDepositsByTypeAndDenom(ctx, params.DepositType, params.DepositDenom, func(deposit types.Deposit) (stop bool) {
|
||||
deposits = append(deposits, deposit)
|
||||
return false
|
||||
})
|
||||
} else if owner && depositType {
|
||||
schedules := k.GetParams(ctx).LiquidityProviderSchedules
|
||||
for _, lps := range schedules {
|
||||
deposit, found := k.GetDeposit(ctx, params.Owner, lps.DepositDenom, params.DepositType)
|
||||
if found {
|
||||
deposits = append(deposits, deposit)
|
||||
}
|
||||
}
|
||||
} else if depositDenom {
|
||||
for _, dt := range types.DepositTypesDepositQuery {
|
||||
k.IterateDepositsByTypeAndDenom(ctx, dt, params.DepositDenom, func(deposit types.Deposit) (stop bool) {
|
||||
deposits = append(deposits, deposit)
|
||||
return false
|
||||
})
|
||||
}
|
||||
} else if owner {
|
||||
schedules := k.GetParams(ctx).LiquidityProviderSchedules
|
||||
for _, lps := range schedules {
|
||||
for _, dt := range types.DepositTypesDepositQuery {
|
||||
deposit, found := k.GetDeposit(ctx, params.Owner, lps.DepositDenom, dt)
|
||||
if found {
|
||||
deposits = append(deposits, deposit)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if depositType {
|
||||
schedules := k.GetParams(ctx).LiquidityProviderSchedules
|
||||
for _, lps := range schedules {
|
||||
k.IterateDepositsByTypeAndDenom(ctx, params.DepositType, lps.DepositDenom, func(deposit types.Deposit) (stop bool) {
|
||||
deposits = append(deposits, deposit)
|
||||
return false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
k.IterateDeposits(ctx, func(deposit types.Deposit) (stop bool) {
|
||||
deposits = append(deposits, deposit)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
start, end := client.Paginate(len(deposits), params.Page, params.Limit, 100)
|
||||
if start < 0 || end < 0 {
|
||||
deposits = []types.Deposit{}
|
||||
} else {
|
||||
deposits = deposits[start:end]
|
||||
}
|
||||
|
||||
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, deposits)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
|
||||
return bz, nil
|
||||
}
|
||||
|
||||
func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
|
||||
var params types.QueryClaimParams
|
||||
err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
|
||||
}
|
||||
depositDenom := false
|
||||
owner := false
|
||||
depositType := false
|
||||
|
||||
if len(params.DepositDenom) > 0 {
|
||||
depositDenom = true
|
||||
}
|
||||
if len(params.Owner) > 0 {
|
||||
owner = true
|
||||
}
|
||||
if len(params.DepositType) > 0 {
|
||||
depositType = true
|
||||
}
|
||||
|
||||
var claims []types.Claim
|
||||
if depositDenom && owner && depositType {
|
||||
claim, found := k.GetClaim(ctx, params.Owner, params.DepositDenom, params.DepositType)
|
||||
if found {
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
} else if depositDenom && owner {
|
||||
for _, dt := range types.DepositTypesClaimQuery {
|
||||
claim, found := k.GetClaim(ctx, params.Owner, params.DepositDenom, dt)
|
||||
if found {
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
}
|
||||
} else if depositDenom && depositType {
|
||||
k.IterateClaimsByTypeAndDenom(ctx, params.DepositType, params.DepositDenom, func(claim types.Claim) (stop bool) {
|
||||
claims = append(claims, claim)
|
||||
return false
|
||||
})
|
||||
} else if owner && depositType {
|
||||
harvestParams := k.GetParams(ctx)
|
||||
for _, lps := range harvestParams.LiquidityProviderSchedules {
|
||||
claim, found := k.GetClaim(ctx, params.Owner, lps.DepositDenom, params.DepositType)
|
||||
if found {
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
}
|
||||
for _, dss := range harvestParams.DelegatorDistributionSchedules {
|
||||
claim, found := k.GetClaim(ctx, params.Owner, dss.DistributionSchedule.DepositDenom, params.DepositType)
|
||||
if found {
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
}
|
||||
} else if depositDenom {
|
||||
for _, dt := range types.DepositTypesClaimQuery {
|
||||
k.IterateClaimsByTypeAndDenom(ctx, dt, params.DepositDenom, func(claim types.Claim) (stop bool) {
|
||||
claims = append(claims, claim)
|
||||
return false
|
||||
})
|
||||
}
|
||||
} else if owner {
|
||||
harvestParams := k.GetParams(ctx)
|
||||
for _, lps := range harvestParams.LiquidityProviderSchedules {
|
||||
for _, dt := range types.DepositTypesClaimQuery {
|
||||
claim, found := k.GetClaim(ctx, params.Owner, lps.DepositDenom, dt)
|
||||
if found {
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dds := range harvestParams.DelegatorDistributionSchedules {
|
||||
for _, dt := range types.DepositTypesClaimQuery {
|
||||
claim, found := k.GetClaim(ctx, params.Owner, dds.DistributionSchedule.DepositDenom, dt)
|
||||
if found {
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if depositType {
|
||||
harvestParams := k.GetParams(ctx)
|
||||
for _, lps := range harvestParams.LiquidityProviderSchedules {
|
||||
k.IterateClaimsByTypeAndDenom(ctx, params.DepositType, lps.DepositDenom, func(claim types.Claim) (stop bool) {
|
||||
claims = append(claims, claim)
|
||||
return false
|
||||
})
|
||||
}
|
||||
for _, dds := range harvestParams.DelegatorDistributionSchedules {
|
||||
k.IterateClaimsByTypeAndDenom(ctx, params.DepositType, dds.DistributionSchedule.DepositDenom, func(claim types.Claim) (stop bool) {
|
||||
claims = append(claims, claim)
|
||||
return false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
k.IterateClaims(ctx, func(claim types.Claim) (stop bool) {
|
||||
claims = append(claims, claim)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
start, end := client.Paginate(len(claims), params.Page, params.Limit, 100)
|
||||
if start < 0 || end < 0 {
|
||||
claims = []types.Claim{}
|
||||
} else {
|
||||
claims = claims[start:end]
|
||||
}
|
||||
|
||||
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, claims)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
|
||||
return bz, nil
|
||||
}
|
166
x/hvt/keeper/rewards.go
Normal file
166
x/hvt/keeper/rewards.go
Normal file
@ -0,0 +1,166 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// ApplyDepositRewards iterates over lp and gov deposits and updates the amount of rewards for each depositor
|
||||
func (k Keeper) ApplyDepositRewards(ctx sdk.Context) {
|
||||
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
|
||||
if !found {
|
||||
previousBlockTime = ctx.BlockTime()
|
||||
k.SetPreviousBlockTime(ctx, previousBlockTime)
|
||||
return
|
||||
}
|
||||
params := k.GetParams(ctx)
|
||||
if !params.Active {
|
||||
return
|
||||
}
|
||||
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
|
||||
|
||||
for _, lps := range params.LiquidityProviderSchedules {
|
||||
if !lps.Active {
|
||||
continue
|
||||
}
|
||||
if lps.End.Before(ctx.BlockTime()) {
|
||||
continue
|
||||
}
|
||||
totalDeposited := k.GetTotalDeposited(ctx, types.LP, lps.DepositDenom)
|
||||
if totalDeposited.IsZero() {
|
||||
continue
|
||||
}
|
||||
rewardsToDistribute := lps.RewardsPerSecond.Amount.Mul(timeElapsed)
|
||||
if rewardsToDistribute.IsZero() {
|
||||
continue
|
||||
}
|
||||
rewardsDistributed := sdk.ZeroInt()
|
||||
k.IterateDepositsByTypeAndDenom(ctx, types.LP, lps.DepositDenom, func(dep types.Deposit) (stop bool) {
|
||||
rewardsShare := sdk.NewDecFromInt(dep.Amount.Amount).Quo(sdk.NewDecFromInt(totalDeposited))
|
||||
if rewardsShare.IsZero() {
|
||||
return false
|
||||
}
|
||||
rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsToDistribute)).RoundInt()
|
||||
if rewardsEarned.IsZero() {
|
||||
return false
|
||||
}
|
||||
k.AddToClaim(ctx, dep.Depositor, dep.Amount.Denom, dep.Type, sdk.NewCoin(lps.RewardsPerSecond.Denom, rewardsEarned))
|
||||
rewardsDistributed = rewardsDistributed.Add(rewardsEarned)
|
||||
return false
|
||||
})
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHarvestLPDistribution,
|
||||
sdk.NewAttribute(types.AttributeKeyBlockHeight, fmt.Sprintf("%d", ctx.BlockHeight())),
|
||||
sdk.NewAttribute(types.AttributeKeyRewardsDistribution, rewardsDistributed.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositDenom, lps.DepositDenom),
|
||||
),
|
||||
)
|
||||
}
|
||||
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
|
||||
}
|
||||
|
||||
// ShouldDistributeValidatorRewards returns true if enough time has elapsed such that rewards should be distributed to delegators
|
||||
func (k Keeper) ShouldDistributeValidatorRewards(ctx sdk.Context, denom string) bool {
|
||||
previousDistributionTime, found := k.GetPreviousDelegatorDistribution(ctx, denom)
|
||||
if !found {
|
||||
k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), denom)
|
||||
return false
|
||||
}
|
||||
params := k.GetParams(ctx)
|
||||
if !params.Active {
|
||||
return false
|
||||
}
|
||||
for _, dds := range params.DelegatorDistributionSchedules {
|
||||
if denom != dds.DistributionSchedule.DepositDenom {
|
||||
continue
|
||||
}
|
||||
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistributionTime.Unix())
|
||||
if timeElapsed.GTE(sdk.NewInt(int64(dds.DistributionFrequency))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ApplyDelegationRewards iterates over each delegation object in the staking store and applies rewards according to the input delegation distribution schedule
|
||||
func (k Keeper) ApplyDelegationRewards(ctx sdk.Context, denom string) {
|
||||
dds, found := k.GetDelegatorSchedule(ctx, denom)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
if !dds.DistributionSchedule.Active {
|
||||
return
|
||||
}
|
||||
bondMacc := k.stakingKeeper.GetBondedPool(ctx)
|
||||
bondedCoinAmount := bondMacc.GetCoins().AmountOf(dds.DistributionSchedule.DepositDenom)
|
||||
if bondedCoinAmount.IsZero() {
|
||||
return
|
||||
}
|
||||
previousDistributionTime, found := k.GetPreviousDelegatorDistribution(ctx, dds.DistributionSchedule.DepositDenom)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistributionTime.Unix())
|
||||
rewardsToDistribute := dds.DistributionSchedule.RewardsPerSecond.Amount.Mul(timeElapsed)
|
||||
|
||||
// create a map that has each validator address (sdk.ValAddress) as a key and the coversion factor for going from delegator shares to tokens for delegations to that validator.
|
||||
// If a validator has never been slashed, the conversion factor will be 1.0, if they have been, it will be < 1.0
|
||||
sharesToTokens := make(map[string]sdk.Dec)
|
||||
k.stakingKeeper.IterateValidators(ctx, func(index int64, validator stakingexported.ValidatorI) (stop bool) {
|
||||
if validator.GetTokens().IsZero() {
|
||||
return false
|
||||
}
|
||||
// don't include a validator if it's unbonded - ie delegators don't accumulate rewards when delegated to an unbonded validator
|
||||
if validator.GetStatus() == sdk.Unbonded {
|
||||
return false
|
||||
}
|
||||
sharesToTokens[validator.GetOperator().String()] = (validator.GetDelegatorShares()).Quo(sdk.NewDecFromInt(validator.GetTokens()))
|
||||
return false
|
||||
})
|
||||
|
||||
rewardsDistributed := sdk.ZeroInt()
|
||||
|
||||
k.stakingKeeper.IterateAllDelegations(ctx, func(delegation stakingtypes.Delegation) (stop bool) {
|
||||
conversionFactor, ok := sharesToTokens[delegation.ValidatorAddress.String()]
|
||||
if ok {
|
||||
delegationTokens := conversionFactor.Mul(delegation.Shares)
|
||||
delegationShare := delegationTokens.Quo(sdk.NewDecFromInt(bondedCoinAmount))
|
||||
rewardsEarned := delegationShare.Mul(sdk.NewDecFromInt(rewardsToDistribute)).RoundInt()
|
||||
if rewardsEarned.IsZero() {
|
||||
return false
|
||||
}
|
||||
k.AddToClaim(
|
||||
ctx, delegation.DelegatorAddress, dds.DistributionSchedule.DepositDenom,
|
||||
types.Stake, sdk.NewCoin(dds.DistributionSchedule.RewardsPerSecond.Denom, rewardsEarned))
|
||||
rewardsDistributed = rewardsDistributed.Add(rewardsEarned)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHarvestDelegatorDistribution,
|
||||
sdk.NewAttribute(types.AttributeKeyBlockHeight, fmt.Sprintf("%d", ctx.BlockHeight())),
|
||||
sdk.NewAttribute(types.AttributeKeyRewardsDistribution, rewardsDistributed.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositDenom, denom),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// AddToClaim adds the input amount to an existing claim or creates a new one
|
||||
func (k Keeper) AddToClaim(ctx sdk.Context, owner sdk.AccAddress, depositDenom string, depositType types.DepositType, amountToAdd sdk.Coin) {
|
||||
claim, found := k.GetClaim(ctx, owner, depositDenom, depositType)
|
||||
if !found {
|
||||
claim = types.NewClaim(owner, depositDenom, amountToAdd, depositType)
|
||||
} else {
|
||||
claim.Amount = claim.Amount.Add(amountToAdd)
|
||||
}
|
||||
k.SetClaim(ctx, claim)
|
||||
}
|
194
x/hvt/keeper/rewards_test.go
Normal file
194
x/hvt/keeper/rewards_test.go
Normal file
@ -0,0 +1,194 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestApplyDepositRewards() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
denom string
|
||||
depositAmount sdk.Coin
|
||||
totalDeposits sdk.Coin
|
||||
rewardRate sdk.Coin
|
||||
depositType types.DepositType
|
||||
previousBlockTime time.Time
|
||||
blockTime time.Time
|
||||
expectedClaimBalance sdk.Coin
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPanic bool
|
||||
contains string
|
||||
}
|
||||
type testCase struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "distribute rewards",
|
||||
args: args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
denom: "bnb",
|
||||
rewardRate: c("hard", 500),
|
||||
depositAmount: c("bnb", 100),
|
||||
totalDeposits: c("bnb", 1000),
|
||||
depositType: types.LP,
|
||||
previousBlockTime: time.Date(2020, 11, 1, 13, 59, 50, 0, time.UTC),
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
expectedClaimBalance: c("hard", 500),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPanic: false,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
config := sdk.GetConfig()
|
||||
app.SetBech32AddressPrefixes(config)
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
), tc.args.previousBlockTime, types.DefaultDistributionTimes)
|
||||
tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount, tc.args.depositType)
|
||||
keeper.SetDeposit(ctx, deposit)
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
if tc.errArgs.expectPanic {
|
||||
suite.Require().Panics(func() { suite.keeper.ApplyDepositRewards(suite.ctx) })
|
||||
} else {
|
||||
suite.Require().NotPanics(func() { suite.keeper.ApplyDepositRewards(suite.ctx) })
|
||||
claim, f := suite.keeper.GetClaim(suite.ctx, tc.args.depositor, tc.args.denom, tc.args.depositType)
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(tc.args.expectedClaimBalance, claim.Amount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestApplyDelegatorRewards() {
|
||||
type args struct {
|
||||
delegator sdk.AccAddress
|
||||
delegatorCoins sdk.Coins
|
||||
delegationAmount sdk.Coin
|
||||
totalBonded sdk.Coin
|
||||
rewardRate sdk.Coin
|
||||
depositType types.DepositType
|
||||
previousDistributionTime time.Time
|
||||
blockTime time.Time
|
||||
expectedClaimBalance sdk.Coin
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPanic bool
|
||||
contains string
|
||||
}
|
||||
type testCase struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "distribute rewards",
|
||||
args: args{
|
||||
delegator: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
delegatorCoins: cs(c("ukava", 1000)),
|
||||
rewardRate: c("hard", 500),
|
||||
delegationAmount: c("ukava", 100),
|
||||
totalBonded: c("ukava", 900),
|
||||
depositType: types.Stake,
|
||||
previousDistributionTime: time.Date(2020, 11, 1, 13, 59, 50, 0, time.UTC),
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
expectedClaimBalance: c("hard", 500),
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPanic: false,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
config := sdk.GetConfig()
|
||||
app.SetBech32AddressPrefixes(config)
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
|
||||
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.delegator, sdk.AccAddress(crypto.AddressHash([]byte("other_delegator")))}, []sdk.Coins{tc.args.delegatorCoins, cs(tc.args.totalBonded)})
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
keeper.SetPreviousDelegationDistribution(ctx, tc.args.previousDistributionTime, "ukava")
|
||||
stakingKeeper := tApp.GetStakingKeeper()
|
||||
stakingParams := stakingKeeper.GetParams(ctx)
|
||||
stakingParams.BondDenom = "ukava"
|
||||
stakingKeeper.SetParams(ctx, stakingParams)
|
||||
validatorPubKey := ed25519.GenPrivKey().PubKey()
|
||||
validator := stakingtypes.NewValidator(sdk.ValAddress(validatorPubKey.Address()), validatorPubKey, stakingtypes.Description{})
|
||||
validator.Status = sdk.Bonded
|
||||
stakingKeeper.SetValidator(ctx, validator)
|
||||
stakingKeeper.SetValidatorByConsAddr(ctx, validator)
|
||||
stakingKeeper.SetNewValidatorByPowerIndex(ctx, validator)
|
||||
// call the after-creation hook
|
||||
stakingKeeper.AfterValidatorCreated(ctx, validator.OperatorAddress)
|
||||
_, err := stakingKeeper.Delegate(ctx, tc.args.delegator, tc.args.delegationAmount.Amount, sdk.Unbonded, validator, true)
|
||||
suite.Require().NoError(err)
|
||||
stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx)
|
||||
validator, f := stakingKeeper.GetValidator(ctx, validator.OperatorAddress)
|
||||
suite.Require().True(f)
|
||||
_, err = stakingKeeper.Delegate(ctx, sdk.AccAddress(crypto.AddressHash([]byte("other_delegator"))), tc.args.totalBonded.Amount, sdk.Unbonded, validator, true)
|
||||
suite.Require().NoError(err)
|
||||
stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx)
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
if tc.errArgs.expectPanic {
|
||||
suite.Require().Panics(func() { suite.keeper.ApplyDelegationRewards(suite.ctx, suite.keeper.BondDenom(suite.ctx)) })
|
||||
} else {
|
||||
suite.Require().NotPanics(func() { suite.keeper.ApplyDelegationRewards(suite.ctx, suite.keeper.BondDenom(suite.ctx)) })
|
||||
claim, f := suite.keeper.GetClaim(suite.ctx, tc.args.delegator, tc.args.delegationAmount.Denom, tc.args.depositType)
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(tc.args.expectedClaimBalance, claim.Amount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
145
x/hvt/keeper/timelock.go
Normal file
145
x/hvt/keeper/timelock.go
Normal file
@ -0,0 +1,145 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
|
||||
supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||
)
|
||||
|
||||
// SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient.
|
||||
// If the recipients account is not a vesting account and the input length is greater than zero,
|
||||
// the recipient account is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length.
|
||||
func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
|
||||
macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule)
|
||||
if !macc.GetCoins().IsAllGTE(amt) {
|
||||
return sdkerrors.Wrapf(types.ErrInsufficientModAccountBalance, "%s", senderModule)
|
||||
}
|
||||
|
||||
// 0. Get the account from the account keeper and do a type switch, error if it's a validator vesting account or module account (can make this work for validator vesting later if necessary)
|
||||
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
|
||||
if acc == nil {
|
||||
return sdkerrors.Wrapf(types.ErrAccountNotFound, recipientAddr.String())
|
||||
}
|
||||
|
||||
if length == 0 {
|
||||
return k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
|
||||
}
|
||||
switch acc.(type) {
|
||||
case *validatorvesting.ValidatorVestingAccount, supplyExported.ModuleAccountI:
|
||||
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
|
||||
case *vesting.PeriodicVestingAccount:
|
||||
return k.SendTimeLockedCoinsToPeriodicVestingAccount(ctx, senderModule, recipientAddr, amt, length)
|
||||
case *auth.BaseAccount:
|
||||
return k.SendTimeLockedCoinsToBaseAccount(ctx, senderModule, recipientAddr, amt, length)
|
||||
default:
|
||||
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
|
||||
}
|
||||
}
|
||||
|
||||
// SendTimeLockedCoinsToPeriodicVestingAccount sends time-locked coins from the input module account to the recipient
|
||||
func (k Keeper) SendTimeLockedCoinsToPeriodicVestingAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
|
||||
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.addCoinsToVestingSchedule(ctx, recipientAddr, amt, length)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendTimeLockedCoinsToBaseAccount sends time-locked coins from the input module account to the recipient, converting the recipient account to a vesting account
|
||||
func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
|
||||
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
|
||||
// transition the account to a periodic vesting account:
|
||||
bacc := authtypes.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
|
||||
newPeriods := vesting.Periods{types.NewPeriod(amt, length)}
|
||||
bva, err := vesting.NewBaseVestingAccount(bacc, amt, ctx.BlockTime().Unix()+length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), newPeriods)
|
||||
k.accountKeeper.SetAccount(ctx, pva)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addCoinsToVestingSchedule adds coins to the input account's vesting schedule where length is the amount of time (from the current block time), in seconds, that the coins will be vesting for
|
||||
// the input address must be a periodic vesting account
|
||||
func (k Keeper) addCoinsToVestingSchedule(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, length int64) {
|
||||
acc := k.accountKeeper.GetAccount(ctx, addr)
|
||||
vacc := acc.(*vesting.PeriodicVestingAccount)
|
||||
// Add the new vesting coins to OriginalVesting
|
||||
vacc.OriginalVesting = vacc.OriginalVesting.Add(amt...)
|
||||
if vacc.EndTime < ctx.BlockTime().Unix() {
|
||||
// edge case one - the vesting account's end time is in the past (ie, all previous vesting periods have completed)
|
||||
// append a new period to the vesting account, update the end time, update the account in the store and return
|
||||
newPeriodLength := (ctx.BlockTime().Unix() - vacc.EndTime) + length
|
||||
newPeriod := types.NewPeriod(amt, newPeriodLength)
|
||||
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
|
||||
vacc.EndTime = ctx.BlockTime().Unix() + length
|
||||
k.accountKeeper.SetAccount(ctx, vacc)
|
||||
return
|
||||
}
|
||||
if vacc.StartTime > ctx.BlockTime().Unix() {
|
||||
// edge case two - the vesting account's start time is in the future (all periods have not started)
|
||||
// update the start time to now and adjust the period lengths in place - a new period will be inserted in the next code block
|
||||
updatedPeriods := vesting.Periods{}
|
||||
for i, period := range vacc.VestingPeriods {
|
||||
updatedPeriod := period
|
||||
if i == 0 {
|
||||
updatedPeriod = types.NewPeriod(period.Amount, (vacc.StartTime-ctx.BlockTime().Unix())+period.Length) // 110 - 100 + 6 = 16
|
||||
}
|
||||
updatedPeriods = append(updatedPeriods, updatedPeriod)
|
||||
}
|
||||
vacc.VestingPeriods = updatedPeriods
|
||||
vacc.StartTime = ctx.BlockTime().Unix()
|
||||
}
|
||||
|
||||
// logic for inserting a new vesting period into the existing vesting schedule
|
||||
remainingLength := vacc.EndTime - ctx.BlockTime().Unix()
|
||||
elapsedTime := ctx.BlockTime().Unix() - vacc.StartTime
|
||||
proposedEndTime := ctx.BlockTime().Unix() + length
|
||||
if remainingLength < length {
|
||||
// in the case that the proposed length is longer than the remaining length of all vesting periods, create a new period with length equal to the difference between the proposed length and the previous total length
|
||||
newPeriodLength := length - remainingLength
|
||||
newPeriod := types.NewPeriod(amt, newPeriodLength)
|
||||
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
|
||||
// update the end time so that the sum of all period lengths equals endTime - startTime
|
||||
vacc.EndTime = proposedEndTime
|
||||
} else {
|
||||
// In the case that the proposed length is less than or equal to the sum of all previous period lengths, insert the period and update other periods as necessary.
|
||||
newPeriods := vesting.Periods{}
|
||||
lengthCounter := int64(0)
|
||||
appendRemaining := false
|
||||
for _, period := range vacc.VestingPeriods {
|
||||
if appendRemaining {
|
||||
newPeriods = append(newPeriods, period)
|
||||
continue
|
||||
}
|
||||
lengthCounter += period.Length
|
||||
if lengthCounter < elapsedTime+length {
|
||||
newPeriods = append(newPeriods, period)
|
||||
} else if lengthCounter == elapsedTime+length {
|
||||
newPeriod := types.NewPeriod(period.Amount.Add(amt...), period.Length)
|
||||
newPeriods = append(newPeriods, newPeriod)
|
||||
appendRemaining = true
|
||||
} else {
|
||||
newPeriod := types.NewPeriod(amt, elapsedTime+length-types.GetTotalVestingPeriodLength(newPeriods))
|
||||
previousPeriod := types.NewPeriod(period.Amount, period.Length-newPeriod.Length)
|
||||
newPeriods = append(newPeriods, newPeriod, previousPeriod)
|
||||
appendRemaining = true
|
||||
}
|
||||
}
|
||||
vacc.VestingPeriods = newPeriods
|
||||
}
|
||||
k.accountKeeper.SetAccount(ctx, vacc)
|
||||
return
|
||||
}
|
334
x/hvt/keeper/timelock_test.go
Normal file
334
x/hvt/keeper/timelock_test.go
Normal file
@ -0,0 +1,334 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
|
||||
type accountArgs struct {
|
||||
addr sdk.AccAddress
|
||||
vestingAccountBefore bool
|
||||
vestingAccountAfter bool
|
||||
coins sdk.Coins
|
||||
periods vesting.Periods
|
||||
origVestingCoins sdk.Coins
|
||||
startTime int64
|
||||
endTime int64
|
||||
}
|
||||
type args struct {
|
||||
accArgs accountArgs
|
||||
period vesting.Period
|
||||
blockTime time.Time
|
||||
expectedAccountBalance sdk.Coins
|
||||
expectedModAccountBalance sdk.Coins
|
||||
expectedPeriods vesting.Periods
|
||||
expectedStartTime int64
|
||||
expectedEndTime int64
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type testCase struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "send liquid coins to base account",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: false,
|
||||
vestingAccountAfter: false,
|
||||
coins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
},
|
||||
period: vesting.Period{Length: 0, Amount: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)))},
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)), sdk.NewCoin("hard", sdk.NewInt(100))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
|
||||
expectedPeriods: vesting.Periods{},
|
||||
expectedStartTime: 0,
|
||||
expectedEndTime: 0,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send liquid coins to vesting account",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: true,
|
||||
vestingAccountAfter: true,
|
||||
coins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
periods: vesting.Periods{
|
||||
vesting.Period{Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), Length: 100},
|
||||
},
|
||||
origVestingCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
startTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
|
||||
endTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix() + 100,
|
||||
},
|
||||
period: vesting.Period{Length: 0, Amount: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)))},
|
||||
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)), sdk.NewCoin("hard", sdk.NewInt(100))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
|
||||
expectedPeriods: vesting.Periods{
|
||||
vesting.Period{Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), Length: 100},
|
||||
},
|
||||
expectedStartTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
|
||||
expectedEndTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix() + 100,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insert period at beginning of schedule",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: true,
|
||||
vestingAccountAfter: true,
|
||||
coins: cs(c("bnb", 20)),
|
||||
periods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
origVestingCoins: cs(c("bnb", 20)),
|
||||
startTime: 100,
|
||||
endTime: 120,
|
||||
},
|
||||
period: vesting.Period{Length: 2, Amount: cs(c("hard", 6))},
|
||||
blockTime: time.Unix(101, 0),
|
||||
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
|
||||
expectedModAccountBalance: cs(c("hard", 994)),
|
||||
expectedPeriods: vesting.Periods{
|
||||
vesting.Period{Length: 3, Amount: cs(c("hard", 6))},
|
||||
vesting.Period{Length: 2, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
expectedStartTime: 100,
|
||||
expectedEndTime: 120,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insert period at beginning with new start time",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: true,
|
||||
vestingAccountAfter: true,
|
||||
coins: cs(c("bnb", 20)),
|
||||
periods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
origVestingCoins: cs(c("bnb", 20)),
|
||||
startTime: 100,
|
||||
endTime: 120,
|
||||
},
|
||||
period: vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
|
||||
blockTime: time.Unix(80, 0),
|
||||
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
|
||||
expectedModAccountBalance: cs(c("hard", 994)),
|
||||
expectedPeriods: vesting.Periods{
|
||||
vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
|
||||
vesting.Period{Length: 18, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
expectedStartTime: 80,
|
||||
expectedEndTime: 120,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insert period in middle of schedule",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: true,
|
||||
vestingAccountAfter: true,
|
||||
coins: cs(c("bnb", 20)),
|
||||
periods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
origVestingCoins: cs(c("bnb", 20)),
|
||||
startTime: 100,
|
||||
endTime: 120,
|
||||
},
|
||||
period: vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
|
||||
blockTime: time.Unix(101, 0),
|
||||
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
|
||||
expectedModAccountBalance: cs(c("hard", 994)),
|
||||
expectedPeriods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 3, Amount: cs(c("hard", 6))},
|
||||
vesting.Period{Length: 2, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
expectedStartTime: 100,
|
||||
expectedEndTime: 120,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "append to end of schedule",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: true,
|
||||
vestingAccountAfter: true,
|
||||
coins: cs(c("bnb", 20)),
|
||||
periods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
origVestingCoins: cs(c("bnb", 20)),
|
||||
startTime: 100,
|
||||
endTime: 120,
|
||||
},
|
||||
period: vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
|
||||
blockTime: time.Unix(125, 0),
|
||||
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
|
||||
expectedModAccountBalance: cs(c("hard", 994)),
|
||||
expectedPeriods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 12, Amount: cs(c("hard", 6))}},
|
||||
expectedStartTime: 100,
|
||||
expectedEndTime: 132,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add coins to existing period",
|
||||
args: args{
|
||||
accArgs: accountArgs{
|
||||
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
vestingAccountBefore: true,
|
||||
vestingAccountAfter: true,
|
||||
coins: cs(c("bnb", 20)),
|
||||
periods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
origVestingCoins: cs(c("bnb", 20)),
|
||||
startTime: 100,
|
||||
endTime: 120,
|
||||
},
|
||||
period: vesting.Period{Length: 5, Amount: cs(c("hard", 6))},
|
||||
blockTime: time.Unix(110, 0),
|
||||
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
|
||||
expectedModAccountBalance: cs(c("hard", 994)),
|
||||
expectedPeriods: vesting.Periods{
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5), c("hard", 6))},
|
||||
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
|
||||
expectedStartTime: 100,
|
||||
expectedEndTime: 120,
|
||||
},
|
||||
errArgs: errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create new app with one funded account
|
||||
config := sdk.GetConfig()
|
||||
app.SetBech32AddressPrefixes(config)
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
|
||||
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.accArgs.addr}, []sdk.Coins{tc.args.accArgs.coins})
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
if tc.args.accArgs.vestingAccountBefore {
|
||||
ak := tApp.GetAccountKeeper()
|
||||
acc := ak.GetAccount(ctx, tc.args.accArgs.addr)
|
||||
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
|
||||
bva, err := vesting.NewBaseVestingAccount(bacc, tc.args.accArgs.origVestingCoins, tc.args.accArgs.endTime)
|
||||
suite.Require().NoError(err)
|
||||
pva := vesting.NewPeriodicVestingAccountRaw(bva, tc.args.accArgs.startTime, tc.args.accArgs.periods)
|
||||
ak.SetAccount(ctx, pva)
|
||||
}
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.LPAccount, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
|
||||
keeper := tApp.GetHarvestKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, types.LPAccount, tc.args.accArgs.addr, tc.args.period.Amount, tc.args.period.Length)
|
||||
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
acc := suite.getAccount(tc.args.accArgs.addr)
|
||||
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
|
||||
mAcc := suite.getModuleAccount(types.LPAccount)
|
||||
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
|
||||
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
|
||||
if tc.args.accArgs.vestingAccountAfter {
|
||||
suite.Require().True(ok)
|
||||
suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods)
|
||||
suite.Require().Equal(tc.args.expectedStartTime, vacc.StartTime)
|
||||
suite.Require().Equal(tc.args.expectedEndTime, vacc.EndTime)
|
||||
} else {
|
||||
suite.Require().False(ok)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
|
||||
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
|
171
x/hvt/module.go
Normal file
171
x/hvt/module.go
Normal file
@ -0,0 +1,171 @@
|
||||
package hvt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
sim "github.com/cosmos/cosmos-sdk/x/simulation"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/client/cli"
|
||||
"github.com/kava-labs/kava/x/hvt/client/rest"
|
||||
"github.com/kava-labs/kava/x/hvt/keeper"
|
||||
"github.com/kava-labs/kava/x/hvt/simulation"
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ module.AppModule = AppModule{}
|
||||
_ module.AppModuleBasic = AppModuleBasic{}
|
||||
_ module.AppModuleSimulation = AppModule{}
|
||||
)
|
||||
|
||||
// AppModuleBasic app module basics object
|
||||
type AppModuleBasic struct{}
|
||||
|
||||
// Name get module name
|
||||
func (AppModuleBasic) Name() string {
|
||||
return ModuleName
|
||||
}
|
||||
|
||||
// RegisterCodec register module codec
|
||||
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
|
||||
RegisterCodec(cdc)
|
||||
}
|
||||
|
||||
// DefaultGenesis default genesis state
|
||||
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
|
||||
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
|
||||
}
|
||||
|
||||
// ValidateGenesis module validate genesis
|
||||
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
err := ModuleCdc.UnmarshalJSON(bz, &gs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gs.Validate()
|
||||
}
|
||||
|
||||
// RegisterRESTRoutes registers REST routes for the harvest module.
|
||||
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
|
||||
rest.RegisterRoutes(ctx, rtr)
|
||||
}
|
||||
|
||||
// GetTxCmd returns the root tx command for the harvest module.
|
||||
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||
return cli.GetTxCmd(cdc)
|
||||
}
|
||||
|
||||
// GetQueryCmd returns no root query command for the harvest module.
|
||||
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
|
||||
return cli.GetQueryCmd(types.StoreKey, cdc)
|
||||
}
|
||||
|
||||
//____________________________________________________________________________
|
||||
|
||||
// AppModule app module type
|
||||
type AppModule struct {
|
||||
AppModuleBasic
|
||||
|
||||
keeper Keeper
|
||||
supplyKeeper types.SupplyKeeper
|
||||
}
|
||||
|
||||
// NewAppModule creates a new AppModule object
|
||||
func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
|
||||
return AppModule{
|
||||
AppModuleBasic: AppModuleBasic{},
|
||||
keeper: keeper,
|
||||
supplyKeeper: supplyKeeper,
|
||||
}
|
||||
}
|
||||
|
||||
// Name module name
|
||||
func (AppModule) Name() string {
|
||||
return ModuleName
|
||||
}
|
||||
|
||||
// RegisterInvariants register module invariants
|
||||
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
|
||||
|
||||
// Route module message route name
|
||||
func (AppModule) Route() string {
|
||||
return ModuleName
|
||||
}
|
||||
|
||||
// NewHandler module handler
|
||||
func (am AppModule) NewHandler() sdk.Handler {
|
||||
return NewHandler(am.keeper)
|
||||
}
|
||||
|
||||
// QuerierRoute module querier route name
|
||||
func (AppModule) QuerierRoute() string {
|
||||
return QuerierRoute
|
||||
}
|
||||
|
||||
// NewQuerierHandler returns no sdk.Querier.
|
||||
func (am AppModule) NewQuerierHandler() sdk.Querier {
|
||||
return keeper.NewQuerier(am.keeper)
|
||||
}
|
||||
|
||||
// InitGenesis module init-genesis
|
||||
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
|
||||
var genesisState GenesisState
|
||||
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
|
||||
InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
|
||||
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
||||
|
||||
// ExportGenesis module export genesis
|
||||
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
|
||||
gs := ExportGenesis(ctx, am.keeper)
|
||||
return ModuleCdc.MustMarshalJSON(gs)
|
||||
}
|
||||
|
||||
// BeginBlock module begin-block
|
||||
func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
|
||||
BeginBlocker(ctx, am.keeper)
|
||||
}
|
||||
|
||||
// EndBlock module end-block
|
||||
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
||||
|
||||
//____________________________________________________________________________
|
||||
|
||||
// GenerateGenesisState creates a randomized GenState of the harvest module
|
||||
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
|
||||
simulation.RandomizedGenState(simState)
|
||||
}
|
||||
|
||||
// ProposalContents doesn't return any content functions for governance proposals.
|
||||
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RandomizedParams returns nil because harvest has no params.
|
||||
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
|
||||
return simulation.ParamChanges(r)
|
||||
}
|
||||
|
||||
// RegisterStoreDecoder registers a decoder for harvest module's types
|
||||
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
|
||||
sdr[StoreKey] = simulation.DecodeStore
|
||||
}
|
||||
|
||||
// WeightedOperations returns the all the harvest module operations with their respective weights.
|
||||
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
|
||||
return nil
|
||||
}
|
36
x/hvt/simulation/decoder.go
Normal file
36
x/hvt/simulation/decoder.go
Normal file
@ -0,0 +1,36 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/kv"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// DecodeStore unmarshals the KVPair's Value to the corresponding harvest type
|
||||
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
|
||||
switch {
|
||||
case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey), bytes.Equal(kvA.Key[:1], types.PreviousDelegationDistributionKey):
|
||||
var timeA, timeB time.Time
|
||||
cdc.MustUnmarshalBinaryBare(kvA.Value, &timeA)
|
||||
cdc.MustUnmarshalBinaryBare(kvB.Value, &timeB)
|
||||
return fmt.Sprintf("%s\n%s", timeA, timeB)
|
||||
case bytes.Equal(kvA.Key[:1], types.DepositsKeyPrefix):
|
||||
var depA, depB types.Deposit
|
||||
cdc.MustUnmarshalBinaryBare(kvA.Value, &depA)
|
||||
cdc.MustUnmarshalBinaryBare(kvB.Value, &depB)
|
||||
return fmt.Sprintf("%s\n%s", depA, depB)
|
||||
case bytes.Equal(kvA.Key[:1], types.ClaimsKeyPrefix):
|
||||
var claimA, claimB types.Claim
|
||||
cdc.MustUnmarshalBinaryBare(kvA.Value, &claimA)
|
||||
cdc.MustUnmarshalBinaryBare(kvB.Value, &claimB)
|
||||
return fmt.Sprintf("%s\n%s", claimA, claimB)
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
|
||||
}
|
||||
}
|
62
x/hvt/simulation/decoder_test.go
Normal file
62
x/hvt/simulation/decoder_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/kv"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
func makeTestCodec() (cdc *codec.Codec) {
|
||||
cdc = codec.New()
|
||||
sdk.RegisterCodec(cdc)
|
||||
codec.RegisterCrypto(cdc)
|
||||
types.RegisterCodec(cdc)
|
||||
return
|
||||
}
|
||||
|
||||
func TestDecodeDistributionStore(t *testing.T) {
|
||||
cdc := makeTestCodec()
|
||||
|
||||
prevBlockTime := time.Now().UTC()
|
||||
deposit := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoin("bnb", sdk.NewInt(1)), "lp")
|
||||
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "stake")
|
||||
|
||||
kvPairs := kv.Pairs{
|
||||
kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryBare(prevBlockTime)},
|
||||
kv.Pair{Key: []byte(types.PreviousDelegationDistributionKey), Value: cdc.MustMarshalBinaryBare(prevBlockTime)},
|
||||
kv.Pair{Key: []byte(types.DepositsKeyPrefix), Value: cdc.MustMarshalBinaryBare(deposit)},
|
||||
kv.Pair{Key: []byte(types.ClaimsKeyPrefix), Value: cdc.MustMarshalBinaryBare(claim)},
|
||||
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedLog string
|
||||
}{
|
||||
{"PreviousBlockTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
|
||||
{"PreviousDistributionTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
|
||||
{"Deposit", fmt.Sprintf("%s\n%s", deposit, deposit)},
|
||||
{"Claim", fmt.Sprintf("%s\n%s", claim, claim)},
|
||||
{"other", ""},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
i, tt := i, tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
switch i {
|
||||
case len(tests) - 1:
|
||||
require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
|
||||
default:
|
||||
require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
79
x/hvt/simulation/genesis.go
Normal file
79
x/hvt/simulation/genesis.go
Normal file
@ -0,0 +1,79 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
// SecondsPerYear is the number of seconds in a year
|
||||
const (
|
||||
SecondsPerYear = 31536000
|
||||
// BaseAprPadding sets the minimum inflation to the calculated SPR inflation rate from being 0.0
|
||||
BaseAprPadding = "0.000000003022265980"
|
||||
)
|
||||
|
||||
// RandomizedGenState generates a random GenesisState for harvest module
|
||||
func RandomizedGenState(simState *module.SimulationState) {
|
||||
// params := genRandomParams(simState)
|
||||
// if err := params.Validate(); err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
|
||||
harvestGenesis := types.DefaultGenesisState()
|
||||
if err := harvestGenesis.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, harvestGenesis))
|
||||
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(harvestGenesis)
|
||||
}
|
||||
|
||||
// func genRandomParams(simState *module.SimulationState) types.Params {
|
||||
// periods := genRandomPeriods(simState.Rand, simState.GenTimestamp)
|
||||
// params := types.NewParams(true, periods)
|
||||
// return params
|
||||
// }
|
||||
|
||||
// func genRandomPeriods(r *rand.Rand, timestamp time.Time) types.Periods {
|
||||
// var periods types.Periods
|
||||
// numPeriods := simulation.RandIntBetween(r, 1, 10)
|
||||
// periodStart := timestamp
|
||||
// for i := 0; i < numPeriods; i++ {
|
||||
// // set periods to be between 1-3 days
|
||||
// durationMultiplier := simulation.RandIntBetween(r, 1, 3)
|
||||
// duration := time.Duration(int64(24*durationMultiplier)) * time.Hour
|
||||
// periodEnd := periodStart.Add(duration)
|
||||
// inflation := genRandomInflation(r)
|
||||
// period := types.NewPeriod(periodStart, periodEnd, inflation)
|
||||
// periods = append(periods, period)
|
||||
// periodStart = periodEnd
|
||||
// }
|
||||
// return periods
|
||||
// }
|
||||
|
||||
// func genRandomInflation(r *rand.Rand) sdk.Dec {
|
||||
// // If sim.RandomDecAmount is less than base apr padding, add base apr padding
|
||||
// aprPadding, _ := sdk.NewDecFromStr(BaseAprPadding)
|
||||
// extraAprInflation := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("0.25"))
|
||||
// for extraAprInflation.LT(aprPadding) {
|
||||
// extraAprInflation = extraAprInflation.Add(aprPadding)
|
||||
// }
|
||||
// aprInflation := sdk.OneDec().Add(extraAprInflation)
|
||||
|
||||
// // convert APR inflation to SPR (inflation per second)
|
||||
// inflationSpr, err := aprInflation.ApproxRoot(uint64(SecondsPerYear))
|
||||
// if err != nil {
|
||||
// panic(fmt.Sprintf("error generating random inflation %v", err))
|
||||
// }
|
||||
// return inflationSpr
|
||||
// }
|
||||
|
||||
// func genRandomActive(r *rand.Rand) bool {
|
||||
// threshold := 50
|
||||
// value := simulation.RandIntBetween(r, 1, 100)
|
||||
// return value > threshold
|
||||
// }
|
13
x/hvt/simulation/params.go
Normal file
13
x/hvt/simulation/params.go
Normal file
@ -0,0 +1,13 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||
)
|
||||
|
||||
// ParamChanges defines the parameters that can be modified by param change proposals
|
||||
// on the simulation
|
||||
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
|
||||
return []simulation.ParamChange{}
|
||||
}
|
22
x/hvt/spec/01_concepts.md
Normal file
22
x/hvt/spec/01_concepts.md
Normal file
@ -0,0 +1,22 @@
|
||||
<!--
|
||||
order: 1
|
||||
-->
|
||||
|
||||
# Concepts
|
||||
|
||||
The harvest module introduces the hard token to the kava blockchain. This module distributes hard tokens to two types of ecosystem participants:
|
||||
|
||||
1. Kava stakers - any address that stakes (delegates) kava tokens will be eligible to claim hard tokens. For each delegator, hard tokens are accumulated ratably based on the total number of kava tokens staked. For example, if a user stakes 1 million KAVA tokens and there are 100 million staked KAVA, that user will accumulate 1% of hard tokens earmarked for stakers during the distribution period. Distribution periods are defined by a start date, an end date, and a number of hard tokens that are distributed per second.
|
||||
2. Depositors - any address that deposits eligible tokens to the harvest module will be eligible to claim hard tokens. For each depositor, hard tokens are accumulated ratable based on the total number of tokens staked of that denomination. For example, if a user deposits 1 million "xyz" tokens and there are 100 million xyz deposited, that user will accumulate 1% of hard tokens earmarked for depositors of that denomination during the distribution period. Distribution periods are defined by a start date, an end date, and a number of hard tokens that are distributed per second.
|
||||
|
||||
Users are not air-dropped tokens, rather they accumulate `Claim` objects that they my submit a transaction in order to claim. In order to better align long term incentives, when users claim hard tokens, they have three options, called 'multipliers', for how tokens are distributed.
|
||||
|
||||
* Liquid - users can immediately receive hard tokens, but they will receive a smaller fraction of tokens than if they choose medium-term or long-term locked tokens.
|
||||
* Medium-term locked - users can receive tokens that are medium-term transfer restricted. They will receive more tokens than users who choose liquid tokens, but fewer than those who choose long term locked tokens.
|
||||
* Long-term locked - users can receive tokens that are long-term transfer restricted. Users choosing this option will receive more tokens than users who choose liquid or medium-term locked tokens.
|
||||
|
||||
The exact multipliers will be voted by governance and can be changed via a governance vote. An example multiplier schedule would be:
|
||||
|
||||
* Liquid - 10% multiplier and no lock up. Users receive 10% as many tokens as users who choose long-term locked tokens.
|
||||
* Medium-term locked - 33% multiplier and 6 month transfer restriction. Users receive 33% as many tokens as users who choose long-term locked tokens.
|
||||
* Long-term locked - 100% multiplier and 2 year transfer restriction. Users receive 10x as many tokens as users who choose liquid tokens and 3x as many tokens as users who choose medium-term locked tokens.
|
52
x/hvt/spec/02_state.md
Normal file
52
x/hvt/spec/02_state.md
Normal file
@ -0,0 +1,52 @@
|
||||
<!--
|
||||
order: 2
|
||||
-->
|
||||
|
||||
# State
|
||||
|
||||
## Parameters and Genesis State
|
||||
|
||||
`Parameters` define the distribution schedule of hard tokens that will be distributed to delegators and depositors, respectively.
|
||||
|
||||
```go
|
||||
// Params governance parameters for harvest module
|
||||
type Params struct {
|
||||
Active bool `json:"active" yaml:"active"`
|
||||
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
|
||||
DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"`
|
||||
}
|
||||
|
||||
// DistributionSchedule distribution schedule for liquidity providers
|
||||
type DistributionSchedule struct {
|
||||
Active bool `json:"active" yaml:"active"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
Start time.Time `json:"start" yaml:"start"`
|
||||
End time.Time `json:"end" yaml:"end"`
|
||||
RewardsPerSecond sdk.Coin `json:"rewards_per_second" yaml:"rewards_per_second"`
|
||||
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
|
||||
ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"`
|
||||
}
|
||||
|
||||
// DistributionSchedules slice of DistributionSchedule
|
||||
type DistributionSchedules []DistributionSchedule
|
||||
|
||||
// DelegatorDistributionSchedule distribution schedule for delegators
|
||||
type DelegatorDistributionSchedule struct {
|
||||
DistributionSchedule DistributionSchedule `json:"distribution_schedule" yaml:"distribution_schedule"`
|
||||
|
||||
DistributionFrequency time.Duration `json:"distribution_frequency" yaml:"distribution_frequency"`
|
||||
}
|
||||
|
||||
// DelegatorDistributionSchedules slice of DelegatorDistributionSchedule
|
||||
type DelegatorDistributionSchedules []DelegatorDistributionSchedule
|
||||
```
|
||||
|
||||
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the harvest module to resume.
|
||||
|
||||
```go
|
||||
// GenesisState is the state that must be provided at genesis.
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
|
||||
}
|
||||
```
|
31
x/hvt/spec/03_messages.md
Normal file
31
x/hvt/spec/03_messages.md
Normal file
@ -0,0 +1,31 @@
|
||||
<!--
|
||||
order: 3
|
||||
-->
|
||||
|
||||
# Messages
|
||||
|
||||
There are three messages in the harvest module. Deposit allows users to deposit assets to the harvest module. In version 2, depositors will be able to use their deposits as collateral to borrow froom harvest. Withdraw removes assets from the harvest module, returning them to the user. Claim allows users to claim earned HARD tokens.
|
||||
|
||||
```go
|
||||
// MsgDeposit deposit asset to the harvest module.
|
||||
type MsgDeposit struct {
|
||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// MsgWithdraw withdraw from the harvest module.
|
||||
type MsgWithdraw struct {
|
||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// MsgClaimReward message type used to claim HARD tokens
|
||||
type MsgClaimReward struct {
|
||||
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
RewardMultiplier string `json:"reward_multiplier" yaml:"reward_multiplier"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
```
|
57
x/hvt/spec/04_events.md
Normal file
57
x/hvt/spec/04_events.md
Normal file
@ -0,0 +1,57 @@
|
||||
<!--
|
||||
order: 4
|
||||
-->
|
||||
|
||||
# Events
|
||||
|
||||
The harvest module emits the following events:
|
||||
|
||||
## Handlers
|
||||
|
||||
### MsgDeposit
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
|----------------------|---------------------|-----------------------|
|
||||
| message | module | harvest |
|
||||
| message | sender | `{sender address}` |
|
||||
| harvest_deposit | amount | `{amount}` |
|
||||
| harvest_deposit | depositor | `{depositor address}` |
|
||||
| harvest_deposit | deposit_denom | `{deposit denom}` |
|
||||
| harvest_deposit | deposit_type | `{deposit type}` |
|
||||
|
||||
### MsgWithdraw
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
|------------------------|---------------------|-----------------------|
|
||||
| message | module | harvest |
|
||||
| message | sender | `{sender address}` |
|
||||
| harvest_deposit | amount | `{amount}` |
|
||||
| harvest_deposit | depositor | `{depositor address}` |
|
||||
| harvest_deposit | deposit_denom | `{deposit denom}` |
|
||||
| harvest_deposit | deposit_type | `{deposit type}` |
|
||||
| delete_harvest_deposit | depositor | `{depositor address}` |
|
||||
| delete_harvest_deposit | deposit_denom | `{deposit denom}` |
|
||||
| delete_harvest_deposit | deposit_type | `{deposit type}` |
|
||||
|
||||
### MsgClaimReward
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
|------------------------|---------------------|--------------------------|
|
||||
| message | module | harvest |
|
||||
| message | sender | `{sender address}` |
|
||||
| claim_harvest_reward | amount | `{amount}` |
|
||||
| claim_harvest_reward | claim_holder | `{claim holder address}` |
|
||||
| claim_harvest_reward | deposit_denom | `{deposit denom}` |
|
||||
| claim_harvest_reward | deposit_type | `{deposit type}` |
|
||||
| claim_harvest_reward | claim_multiplier | `{claim multiplier}` |
|
||||
|
||||
## BeginBlock
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
|--------------------------------|---------------------|--------------------------|
|
||||
| harvest_lp_distribution | block_height | `{block height}` |
|
||||
| harvest_lp_distribution | rewards_distributed | `{rewards distributed}` |
|
||||
| harvest_lp_distribution | deposit_denom | `{deposit denom}` |
|
||||
| harvest_delegator_distribution | block_height | `{block height}` |
|
||||
| harvest_delegator_distribution | rewards_distributed | `{rewards distributed}` |
|
||||
| harvest_delegator_distribution | deposit_denom | `{deposit denom}` |
|
46
x/hvt/spec/05_params.md
Normal file
46
x/hvt/spec/05_params.md
Normal file
@ -0,0 +1,46 @@
|
||||
<!--
|
||||
order: 5
|
||||
-->
|
||||
|
||||
# Parameters
|
||||
|
||||
The harvest module has the following parameters:
|
||||
|
||||
| Key | Type | Example | Description |
|
||||
|-----------------------------------|---------------------------------------|---------------|--------------------------------------------------|
|
||||
| Active | bool | "true" | boolean for if token distribution is active |
|
||||
| LiquidityProviderSchedules | array (LiquidityProviderSchedule) | [{see below}] | array of params for each supported asset |
|
||||
| DelegatorDistributionSchedules | array (DelegatorDistributionSchedule) | [{see below}] | array of params for staking incentive assets |
|
||||
|
||||
Each `LiquidityProviderSchedules` has the following parameters
|
||||
|
||||
| Key | Type | Example | Description |
|
||||
|------------------|--------------------|--------------------------|----------------------------------------------------------------|
|
||||
| Active | bool | "true" | boolean for if token distribution is active for this schedule |
|
||||
| DepositDenom | string | "bnb" | coin denom of the asset which can be deposited |
|
||||
| Start | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
|
||||
| End | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
|
||||
| RewardsPerSecond | Coin | "500hard" | HARD tokens per second that can be claimed by depositors |
|
||||
| ClaimEnd | time.Time | "2022-06-01T15:20:00Z" | the time at which users can no longer claim HARD tokens |
|
||||
| ClaimMultipliers | array (Multiplier) | [{see below}] | reward multipliers for users claiming HARD tokens |
|
||||
|
||||
Each `DelegatorDistributionSchedule` has the following parameters
|
||||
|
||||
| Key | Type | Example | Description |
|
||||
|-----------------------|--------------------|--------------------------|----------------------------------------------------------------|
|
||||
| Active | bool | "true" | boolean for if token distribution is active for this schedule |
|
||||
| DepositDenom | string | "bnb" | coin denom of the asset which can be deposited |
|
||||
| Start | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
|
||||
| End | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
|
||||
| RewardsPerSecond | Coin | "500hard" | HARD tokens per second that can be claimed by depositors |
|
||||
| ClaimEnd | time.Time | "2022-06-01T15:20:00Z" | the time at which users can no longer claim HARD tokens |
|
||||
| ClaimMultipliers | array (Multiplier) | [{see below}] | reward multipliers for users claiming HARD tokens |
|
||||
| DistributionFrequency | time.Duration | "24hr" | frequency at which delegation rewards are accumulated |
|
||||
|
||||
Each `ClaimMultiplier` has the following parameters
|
||||
|
||||
| Key | Type | Example | Description |
|
||||
|-----------------------|--------------------|--------------------------|-----------------------------------------------------------------|
|
||||
| Name | string | "large" | the unique name of the reward multiplier |
|
||||
| MonthsLockup | int | "6" | number of months HARD tokens with this multiplier are locked |
|
||||
| Factor | Dec | "0.5" | the scaling factor for HARD tokens claimed with this multiplier |
|
19
x/hvt/spec/06_begin_block.md
Normal file
19
x/hvt/spec/06_begin_block.md
Normal file
@ -0,0 +1,19 @@
|
||||
<!--
|
||||
order: 6
|
||||
-->
|
||||
|
||||
# Begin Block
|
||||
|
||||
At the start of each block, hard tokens are distributed (as claims) to liquidity providers and delegators, respectively.
|
||||
|
||||
```go
|
||||
// BeginBlocker applies rewards to liquidity providers and delegators according to params
|
||||
func BeginBlocker(ctx sdk.Context, k Keeper) {
|
||||
k.ApplyDepositRewards(ctx)
|
||||
if k.ShouldDistributeValidatorRewards(ctx, k.BondDenom(ctx)) {
|
||||
k.ApplyDelegationRewards(ctx, k.BondDenom(ctx))
|
||||
k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), k.BondDenom(ctx))
|
||||
}
|
||||
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
|
||||
}
|
||||
```
|
20
x/hvt/spec/README.md
Normal file
20
x/hvt/spec/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
<!--
|
||||
order: 0
|
||||
title: "Harvest Overview"
|
||||
parent:
|
||||
title: "harvest"
|
||||
-->
|
||||
|
||||
# `harvest`
|
||||
|
||||
<!-- TOC -->
|
||||
1. **[Concepts](01_concepts.md)**
|
||||
2. **[State](02_state.md)**
|
||||
3. **[Messages](03_messages.md)**
|
||||
4. **[Events](04_events.md)**
|
||||
5. **[Params](05_params.md)**
|
||||
6. **[BeginBlock](06_begin_block.md)**
|
||||
|
||||
## Abstract
|
||||
|
||||
`x/hvt` is an implementation of a Cosmos SDK Module that will serve as the basis for a cross-chain money market platform. The current version of the module defines how HARD tokens are distributed, while future versions of this module will define lending, borrowing, distribution, incentives, and governance for the money market module.
|
23
x/hvt/types/claim.go
Normal file
23
x/hvt/types/claim.go
Normal file
@ -0,0 +1,23 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Claim defines an amount of coins that the owner can claim
|
||||
type Claim struct {
|
||||
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
Type DepositType `json:"type" yaml:"type"`
|
||||
}
|
||||
|
||||
// NewClaim returns a new claim
|
||||
func NewClaim(owner sdk.AccAddress, denom string, amount sdk.Coin, dtype DepositType) Claim {
|
||||
return Claim{
|
||||
Owner: owner,
|
||||
DepositDenom: denom,
|
||||
Amount: amount,
|
||||
Type: dtype,
|
||||
}
|
||||
}
|
21
x/hvt/types/codec.go
Normal file
21
x/hvt/types/codec.go
Normal file
@ -0,0 +1,21 @@
|
||||
package types
|
||||
|
||||
import "github.com/cosmos/cosmos-sdk/codec"
|
||||
|
||||
// ModuleCdc generic sealed codec to be used throughout module
|
||||
var ModuleCdc *codec.Codec
|
||||
|
||||
func init() {
|
||||
cdc := codec.New()
|
||||
RegisterCodec(cdc)
|
||||
codec.RegisterCrypto(cdc)
|
||||
ModuleCdc = cdc.Seal()
|
||||
}
|
||||
|
||||
// RegisterCodec registers the necessary types for hvt module
|
||||
func RegisterCodec(cdc *codec.Codec) {
|
||||
cdc.RegisterConcrete(MsgClaimReward{}, "hvt/MsgClaimReward", nil)
|
||||
cdc.RegisterConcrete(MsgDeposit{}, "hvt/MsgDeposit", nil)
|
||||
cdc.RegisterConcrete(MsgWithdraw{}, "hvt/MsgWithdraw", nil)
|
||||
cdc.RegisterConcrete(DistributionSchedule{}, "hvt/DistributionSchedule", nil)
|
||||
}
|
21
x/hvt/types/deposit.go
Normal file
21
x/hvt/types/deposit.go
Normal file
@ -0,0 +1,21 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Deposit defines an amount of coins deposited into a harvest module account
|
||||
type Deposit struct {
|
||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
Type DepositType `json:"type" yaml:"type"`
|
||||
}
|
||||
|
||||
// NewDeposit returns a new deposit
|
||||
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coin, dtype DepositType) Deposit {
|
||||
return Deposit{
|
||||
Depositor: depositor,
|
||||
Amount: amount,
|
||||
Type: dtype,
|
||||
}
|
||||
}
|
38
x/hvt/types/errors.go
Normal file
38
x/hvt/types/errors.go
Normal file
@ -0,0 +1,38 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
)
|
||||
|
||||
// DONTCOVER
|
||||
|
||||
var (
|
||||
// ErrInvalidDepositDenom error for invalid deposit denoms
|
||||
ErrInvalidDepositDenom = sdkerrors.Register(ModuleName, 2, "invalid deposit denom")
|
||||
// ErrDepositNotFound error for deposit not found
|
||||
ErrDepositNotFound = sdkerrors.Register(ModuleName, 3, "deposit not found")
|
||||
// ErrInvaliWithdrawAmount error for invalid withdrawal amount
|
||||
ErrInvaliWithdrawAmount = sdkerrors.Register(ModuleName, 4, "withdrawal amount exceeds deposit amount")
|
||||
// ErrInvalidDepositType error for invalid deposit type
|
||||
ErrInvalidDepositType = sdkerrors.Register(ModuleName, 5, "invalid deposit type")
|
||||
// ErrClaimNotFound error for claim not found
|
||||
ErrClaimNotFound = sdkerrors.Register(ModuleName, 6, "claim not found")
|
||||
// ErrZeroClaim error for claim amount rounded to zero
|
||||
ErrZeroClaim = sdkerrors.Register(ModuleName, 7, "cannot claim - claim amount rounds to zero")
|
||||
// ErrLPScheduleNotFound error for liquidity provider rewards schedule not found
|
||||
ErrLPScheduleNotFound = sdkerrors.Register(ModuleName, 8, "no liquidity provider rewards schedule found")
|
||||
// ErrGovScheduleNotFound error for governance distribution rewards schedule not found
|
||||
ErrGovScheduleNotFound = sdkerrors.Register(ModuleName, 9, "no governance rewards schedule found")
|
||||
// ErrInvalidMultiplier error for multiplier not found
|
||||
ErrInvalidMultiplier = sdkerrors.Register(ModuleName, 10, "invalid rewards multiplier")
|
||||
// ErrInsufficientModAccountBalance error for module account with innsufficient balance
|
||||
ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 11, "module account has insufficient balance to pay reward")
|
||||
// ErrInvalidAccountType error for unsupported accounts
|
||||
ErrInvalidAccountType = sdkerrors.Register(ModuleName, 12, "receiver account type not supported")
|
||||
// ErrAccountNotFound error for accounts that are not found in state
|
||||
ErrAccountNotFound = sdkerrors.Register(ModuleName, 13, "account not found")
|
||||
// ErrClaimExpired error for expired claims
|
||||
ErrClaimExpired = sdkerrors.Register(ModuleName, 14, "claim period expired")
|
||||
// ErrInvalidReceiver error for when sending and receiving accounts don't match
|
||||
ErrInvalidReceiver = sdkerrors.Register(ModuleName, 15, "receiver account must match sender account")
|
||||
)
|
21
x/hvt/types/events.go
Normal file
21
x/hvt/types/events.go
Normal file
@ -0,0 +1,21 @@
|
||||
package types
|
||||
|
||||
// Event types for harvest module
|
||||
const (
|
||||
EventTypeHarvestDeposit = "harvest_deposit"
|
||||
EventTypeHarvestDelegatorDistribution = "harvest_delegator_distribution"
|
||||
EventTypeHarvestLPDistribution = "harvest_lp_distribution"
|
||||
EventTypeDeleteHarvestDeposit = "delete_harvest_deposit"
|
||||
EventTypeHarvestWithdrawal = "harvest_withdrawal"
|
||||
EventTypeClaimHarvestReward = "claim_harvest_reward"
|
||||
AttributeValueCategory = ModuleName
|
||||
AttributeKeyBlockHeight = "block_height"
|
||||
AttributeKeyRewardsDistribution = "rewards_distributed"
|
||||
AttributeKeyDeposit = "deposit"
|
||||
AttributeKeyDepositType = "deposit_type"
|
||||
AttributeKeyDepositDenom = "deposit_denom"
|
||||
AttributeKeyDepositor = "depositor"
|
||||
AttributeKeyClaimHolder = "claim_holder"
|
||||
AttributeKeyClaimAmount = "claim_amount"
|
||||
AttributeKeyClaimMultiplier = "claim_multiplier"
|
||||
)
|
35
x/hvt/types/expected_keepers.go
Normal file
35
x/hvt/types/expected_keepers.go
Normal file
@ -0,0 +1,35 @@
|
||||
package types // noalias
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
|
||||
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/supply/exported"
|
||||
)
|
||||
|
||||
// SupplyKeeper defines the expected supply keeper
|
||||
type SupplyKeeper interface {
|
||||
GetModuleAddress(name string) sdk.AccAddress
|
||||
GetModuleAccount(ctx sdk.Context, name string) exported.ModuleAccountI
|
||||
GetSupply(ctx sdk.Context) (supply exported.SupplyI)
|
||||
SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error
|
||||
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
|
||||
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
|
||||
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
|
||||
}
|
||||
|
||||
// AccountKeeper defines the expected keeper interface for interacting with account
|
||||
type AccountKeeper interface {
|
||||
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
|
||||
SetAccount(ctx sdk.Context, acc authexported.Account)
|
||||
}
|
||||
|
||||
// StakingKeeper defines the expected keeper interface for the staking keeper
|
||||
type StakingKeeper interface {
|
||||
IterateLastValidators(ctx sdk.Context, fn func(index int64, validator stakingexported.ValidatorI) (stop bool))
|
||||
IterateValidators(sdk.Context, func(index int64, validator stakingexported.ValidatorI) (stop bool))
|
||||
IterateAllDelegations(ctx sdk.Context, cb func(delegation stakingtypes.Delegation) (stop bool))
|
||||
GetBondedPool(ctx sdk.Context) (bondedPool exported.ModuleAccountI)
|
||||
BondDenom(ctx sdk.Context) (res string)
|
||||
}
|
84
x/hvt/types/genesis.go
Normal file
84
x/hvt/types/genesis.go
Normal file
@ -0,0 +1,84 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
)
|
||||
|
||||
// GenesisState default values
|
||||
var (
|
||||
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
|
||||
DefaultDistributionTimes = GenesisDistributionTimes{}
|
||||
)
|
||||
|
||||
// GenesisState is the state that must be provided at genesis.
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
|
||||
PreviousDistributionTimes GenesisDistributionTimes `json:"previous_distribution_times" yaml:"previous_distribution_times"`
|
||||
}
|
||||
|
||||
// NewGenesisState returns a new genesis state
|
||||
func NewGenesisState(params Params, previousBlockTime time.Time, previousDistTimes GenesisDistributionTimes) GenesisState {
|
||||
return GenesisState{
|
||||
Params: params,
|
||||
PreviousBlockTime: previousBlockTime,
|
||||
PreviousDistributionTimes: previousDistTimes,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultGenesisState returns a default genesis state
|
||||
func DefaultGenesisState() GenesisState {
|
||||
return GenesisState{
|
||||
Params: DefaultParams(),
|
||||
PreviousBlockTime: DefaultPreviousBlockTime,
|
||||
PreviousDistributionTimes: DefaultDistributionTimes,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs basic validation of genesis data returning an
|
||||
// error for any failed validation criteria.
|
||||
func (gs GenesisState) Validate() error {
|
||||
|
||||
if err := gs.Params.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if gs.PreviousBlockTime.Equal(time.Time{}) {
|
||||
return fmt.Errorf("previous block time not set")
|
||||
}
|
||||
for _, gdt := range gs.PreviousDistributionTimes {
|
||||
if gdt.PreviousDistributionTime.Equal(time.Time{}) {
|
||||
return fmt.Errorf("previous distribution time not set for %s", gdt.Denom)
|
||||
}
|
||||
if err := sdk.ValidateDenom(gdt.Denom); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Equal checks whether two gov GenesisState structs are equivalent
|
||||
func (gs GenesisState) Equal(gs2 GenesisState) bool {
|
||||
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
|
||||
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
|
||||
return bytes.Equal(b1, b2)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if a GenesisState is empty
|
||||
func (gs GenesisState) IsEmpty() bool {
|
||||
return gs.Equal(GenesisState{})
|
||||
}
|
||||
|
||||
// GenesisDistributionTime stores the previous distribution time and its corresponding denom
|
||||
type GenesisDistributionTime struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"`
|
||||
}
|
||||
|
||||
// GenesisDistributionTimes slice of GenesisDistributionTime
|
||||
type GenesisDistributionTimes []GenesisDistributionTime
|
124
x/hvt/types/genesis_test.go
Normal file
124
x/hvt/types/genesis_test.go
Normal file
@ -0,0 +1,124 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
type GenesisTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *GenesisTestSuite) TestGenesisValidation() {
|
||||
type args struct {
|
||||
params types.Params
|
||||
pbt time.Time
|
||||
pdts types.GenesisDistributionTimes
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
expectPass bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
args: args{
|
||||
params: types.DefaultParams(),
|
||||
pbt: types.DefaultPreviousBlockTime,
|
||||
pdts: types.DefaultDistributionTimes,
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
params: types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.OneDec()), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("1.5")), types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("3"))}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
),
|
||||
pbt: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC),
|
||||
pdts: types.GenesisDistributionTimes{
|
||||
{PreviousDistributionTime: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC), Denom: "bnb"},
|
||||
},
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid previous blocktime",
|
||||
args: args{
|
||||
params: types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.OneDec()), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("1.5")), types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("3"))}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
),
|
||||
pbt: time.Time{},
|
||||
pdts: types.GenesisDistributionTimes{
|
||||
{PreviousDistributionTime: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC), Denom: "bnb"},
|
||||
},
|
||||
},
|
||||
expectPass: false,
|
||||
expectedErr: "previous block time not set",
|
||||
},
|
||||
{
|
||||
name: "invalid previous distribution time",
|
||||
args: args{
|
||||
params: types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.OneDec()), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("1.5")), types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("3"))}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
),
|
||||
pbt: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC),
|
||||
pdts: types.GenesisDistributionTimes{
|
||||
{PreviousDistributionTime: time.Time{}, Denom: "bnb"},
|
||||
},
|
||||
},
|
||||
expectPass: false,
|
||||
expectedErr: "previous distribution time not set",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
gs := types.NewGenesisState(tc.args.params, tc.args.pbt, tc.args.pdts)
|
||||
err := gs.Validate()
|
||||
if tc.expectPass {
|
||||
suite.NoError(err)
|
||||
} else {
|
||||
suite.Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenesisTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(GenesisTestSuite))
|
||||
}
|
61
x/hvt/types/keys.go
Normal file
61
x/hvt/types/keys.go
Normal file
@ -0,0 +1,61 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// ModuleName name that will be used throughout the module
|
||||
ModuleName = "harvest"
|
||||
|
||||
// LPAccount LP distribution module account
|
||||
LPAccount = "harvest_lp_distribution"
|
||||
|
||||
// DelegatorAccount delegator distribution module account
|
||||
DelegatorAccount = "harvest_delegator_distribution"
|
||||
|
||||
// ModuleAccountName name of module account used to hold deposits
|
||||
ModuleAccountName = "harvest"
|
||||
|
||||
// StoreKey Top level store key where all module items will be stored
|
||||
StoreKey = ModuleName
|
||||
|
||||
// RouterKey Top level router key
|
||||
RouterKey = ModuleName
|
||||
|
||||
// QuerierRoute Top level query string
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// DefaultParamspace default name for parameter store
|
||||
DefaultParamspace = ModuleName
|
||||
)
|
||||
|
||||
var (
|
||||
PreviousBlockTimeKey = []byte{0x01}
|
||||
PreviousDelegationDistributionKey = []byte{0x02}
|
||||
DepositsKeyPrefix = []byte{0x03}
|
||||
ClaimsKeyPrefix = []byte{0x04}
|
||||
sep = []byte(":")
|
||||
)
|
||||
|
||||
// DepositKey key of a specific deposit in the store
|
||||
func DepositKey(depositType DepositType, denom string, depositor sdk.AccAddress) []byte {
|
||||
return createKey([]byte(depositType), sep, []byte(denom), sep, depositor)
|
||||
}
|
||||
|
||||
// DepositTypeIteratorKey returns an interator prefix for interating over deposits by deposit type and denom
|
||||
func DepositTypeIteratorKey(depositType DepositType, denom string) []byte {
|
||||
return createKey([]byte(depositType), sep, []byte(denom))
|
||||
}
|
||||
|
||||
// ClaimKey key of a specific deposit in the store
|
||||
func ClaimKey(depositType DepositType, denom string, owner sdk.AccAddress) []byte {
|
||||
return createKey([]byte(depositType), sep, []byte(denom), sep, owner)
|
||||
}
|
||||
|
||||
func createKey(bytes ...[]byte) (r []byte) {
|
||||
for _, b := range bytes {
|
||||
r = append(r, b...)
|
||||
}
|
||||
return
|
||||
}
|
216
x/hvt/types/msg.go
Normal file
216
x/hvt/types/msg.go
Normal file
@ -0,0 +1,216 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
)
|
||||
|
||||
// MultiplierName name for valid multiplier
|
||||
type MultiplierName string
|
||||
|
||||
// DepositType type for valid deposit type strings
|
||||
type DepositType string
|
||||
|
||||
// Valid reward multipliers and reward types
|
||||
const (
|
||||
Small MultiplierName = "small"
|
||||
Medium MultiplierName = "medium"
|
||||
Large MultiplierName = "large"
|
||||
|
||||
LP DepositType = "lp"
|
||||
Stake DepositType = "stake"
|
||||
)
|
||||
|
||||
// Queryable deposit types
|
||||
var (
|
||||
DepositTypesDepositQuery = []DepositType{LP}
|
||||
DepositTypesClaimQuery = []DepositType{LP, Stake}
|
||||
)
|
||||
|
||||
// IsValid checks if the input is one of the expected strings
|
||||
func (mn MultiplierName) IsValid() error {
|
||||
switch mn {
|
||||
case Small, Medium, Large:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid multiplier name: %s", mn)
|
||||
}
|
||||
|
||||
// IsValid checks if the input is one of the expected strings
|
||||
func (dt DepositType) IsValid() error {
|
||||
switch dt {
|
||||
case LP, Stake:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid deposit type: %s", dt)
|
||||
}
|
||||
|
||||
// ensure Msg interface compliance at compile time
|
||||
var (
|
||||
_ sdk.Msg = &MsgClaimReward{}
|
||||
_ sdk.Msg = &MsgDeposit{}
|
||||
_ sdk.Msg = &MsgWithdraw{}
|
||||
)
|
||||
|
||||
// MsgDeposit deposit collateral to the harvest module.
|
||||
type MsgDeposit struct {
|
||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// NewMsgDeposit returns a new MsgDeposit
|
||||
func NewMsgDeposit(depositor sdk.AccAddress, amount sdk.Coin, depositType string) MsgDeposit {
|
||||
return MsgDeposit{
|
||||
Depositor: depositor,
|
||||
Amount: amount,
|
||||
DepositType: depositType,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgDeposit) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgDeposit) Type() string { return "harvest_deposit" }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||
func (msg MsgDeposit) ValidateBasic() error {
|
||||
if msg.Depositor.Empty() {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||
}
|
||||
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "deposit amount %s", msg.Amount)
|
||||
}
|
||||
return DepositType(strings.ToLower(msg.DepositType)).IsValid()
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgDeposit) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgDeposit) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{msg.Depositor}
|
||||
}
|
||||
|
||||
// String implements the Stringer interface
|
||||
func (msg MsgDeposit) String() string {
|
||||
return fmt.Sprintf(`Deposit Message:
|
||||
Depositor: %s
|
||||
Amount: %s
|
||||
Deposit Type: %s
|
||||
`, msg.Depositor, msg.Amount, msg.DepositType)
|
||||
}
|
||||
|
||||
// MsgWithdraw withdraw from the harvest module.
|
||||
type MsgWithdraw struct {
|
||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||
Amount sdk.Coin `json:"amount" yaml:"amount"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// NewMsgWithdraw returns a new MsgWithdraw
|
||||
func NewMsgWithdraw(depositor sdk.AccAddress, amount sdk.Coin, depositType string) MsgDeposit {
|
||||
return MsgDeposit{
|
||||
Depositor: depositor,
|
||||
Amount: amount,
|
||||
DepositType: depositType,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgWithdraw) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgWithdraw) Type() string { return "harvest_withdraw" }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||
func (msg MsgWithdraw) ValidateBasic() error {
|
||||
if msg.Depositor.Empty() {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||
}
|
||||
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "deposit amount %s", msg.Amount)
|
||||
}
|
||||
return DepositType(strings.ToLower(msg.DepositType)).IsValid()
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgWithdraw) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgWithdraw) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{msg.Depositor}
|
||||
}
|
||||
|
||||
// String implements the Stringer interface
|
||||
func (msg MsgWithdraw) String() string {
|
||||
return fmt.Sprintf(`Withdraw Message:
|
||||
Depositor: %s
|
||||
Amount: %s
|
||||
Deposit Type: %s
|
||||
`, msg.Depositor, msg.Amount, msg.DepositType)
|
||||
}
|
||||
|
||||
// MsgClaimReward message type used to claim rewards
|
||||
type MsgClaimReward struct {
|
||||
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"`
|
||||
DepositType string `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// NewMsgClaimReward returns a new MsgClaimReward.
|
||||
func NewMsgClaimReward(sender, receiver sdk.AccAddress, depositDenom, depositType, multiplier string) MsgClaimReward {
|
||||
return MsgClaimReward{
|
||||
Sender: sender,
|
||||
Receiver: receiver,
|
||||
DepositDenom: depositDenom,
|
||||
MultiplierName: multiplier,
|
||||
DepositType: depositType,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgClaimReward) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgClaimReward) Type() string { return "claim_harvest_reward" }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to state.
|
||||
func (msg MsgClaimReward) ValidateBasic() error {
|
||||
if msg.Sender.Empty() {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||
}
|
||||
if msg.Receiver.Empty() {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty")
|
||||
}
|
||||
if err := sdk.ValidateDenom(msg.DepositDenom); err != nil {
|
||||
return fmt.Errorf("collateral type cannot be blank")
|
||||
}
|
||||
if err := DepositType(strings.ToLower(msg.DepositType)).IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid()
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgClaimReward) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{msg.Sender}
|
||||
}
|
223
x/hvt/types/msg_test.go
Normal file
223
x/hvt/types/msg_test.go
Normal file
@ -0,0 +1,223 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
type MsgTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *MsgTestSuite) TestMsgDeposit() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
amount sdk.Coin
|
||||
depositType string
|
||||
}
|
||||
addrs := []sdk.AccAddress{
|
||||
sdk.AccAddress("test1"),
|
||||
sdk.AccAddress("test2"),
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
expectPass bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
depositor: addrs[0],
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
|
||||
depositType: "lp",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid2",
|
||||
args: args{
|
||||
depositor: addrs[0],
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
|
||||
depositType: "LP",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
depositor: addrs[0],
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
|
||||
depositType: "cat",
|
||||
},
|
||||
expectPass: false,
|
||||
expectedErr: "invalid deposit type",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
msg := types.NewMsgDeposit(tc.args.depositor, tc.args.amount, tc.args.depositType)
|
||||
err := msg.ValidateBasic()
|
||||
if tc.expectPass {
|
||||
suite.NoError(err)
|
||||
} else {
|
||||
suite.Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *MsgTestSuite) TestMsgWithdraw() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
amount sdk.Coin
|
||||
depositType string
|
||||
}
|
||||
addrs := []sdk.AccAddress{
|
||||
sdk.AccAddress("test1"),
|
||||
sdk.AccAddress("test2"),
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
expectPass bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
depositor: addrs[0],
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
|
||||
depositType: "lp",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid2",
|
||||
args: args{
|
||||
depositor: addrs[0],
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
|
||||
depositType: "LP",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
depositor: addrs[0],
|
||||
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
|
||||
depositType: "cat",
|
||||
},
|
||||
expectPass: false,
|
||||
expectedErr: "invalid deposit type",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
msg := types.NewMsgWithdraw(tc.args.depositor, tc.args.amount, tc.args.depositType)
|
||||
err := msg.ValidateBasic()
|
||||
if tc.expectPass {
|
||||
suite.NoError(err)
|
||||
} else {
|
||||
suite.Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *MsgTestSuite) TestMsgClaim() {
|
||||
type args struct {
|
||||
sender sdk.AccAddress
|
||||
receiver sdk.AccAddress
|
||||
denom string
|
||||
depositType string
|
||||
multiplier string
|
||||
}
|
||||
addrs := []sdk.AccAddress{
|
||||
sdk.AccAddress("test1"),
|
||||
sdk.AccAddress("test2"),
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
expectPass bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
sender: addrs[0],
|
||||
receiver: addrs[0],
|
||||
denom: "bnb",
|
||||
depositType: "lp",
|
||||
multiplier: "large",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid2",
|
||||
args: args{
|
||||
sender: addrs[0],
|
||||
receiver: addrs[0],
|
||||
denom: "bnb",
|
||||
depositType: "stake",
|
||||
multiplier: "small",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid3",
|
||||
args: args{
|
||||
sender: addrs[0],
|
||||
receiver: addrs[1],
|
||||
denom: "bnb",
|
||||
depositType: "lp",
|
||||
multiplier: "Medium",
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
args: args{
|
||||
sender: addrs[0],
|
||||
receiver: addrs[0],
|
||||
denom: "bnb",
|
||||
depositType: "lp",
|
||||
multiplier: "huge",
|
||||
},
|
||||
expectPass: false,
|
||||
expectedErr: "invalid multiplier name",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
msg := types.NewMsgClaimReward(tc.args.sender, tc.args.receiver, tc.args.denom, tc.args.depositType, tc.args.multiplier)
|
||||
err := msg.ValidateBasic()
|
||||
if tc.expectPass {
|
||||
suite.NoError(err)
|
||||
} else {
|
||||
suite.Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MsgTestSuite))
|
||||
}
|
299
x/hvt/types/params.go
Normal file
299
x/hvt/types/params.go
Normal file
@ -0,0 +1,299 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/params"
|
||||
|
||||
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
||||
)
|
||||
|
||||
// Parameter keys and default values
|
||||
var (
|
||||
KeyActive = []byte("Active")
|
||||
KeyLPSchedules = []byte("LPSchedules")
|
||||
KeyDelegatorSchedule = []byte("DelegatorSchedule")
|
||||
DefaultActive = true
|
||||
DefaultGovSchedules = DistributionSchedules{}
|
||||
DefaultLPSchedules = DistributionSchedules{}
|
||||
DefaultDelegatorSchedules = DelegatorDistributionSchedules{}
|
||||
GovDenom = cdptypes.DefaultGovDenom
|
||||
)
|
||||
|
||||
// Params governance parameters for harvest module
|
||||
type Params struct {
|
||||
Active bool `json:"active" yaml:"active"`
|
||||
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
|
||||
DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"`
|
||||
}
|
||||
|
||||
// DistributionSchedule distribution schedule for liquidity providers
|
||||
type DistributionSchedule struct {
|
||||
Active bool `json:"active" yaml:"active"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
Start time.Time `json:"start" yaml:"start"`
|
||||
End time.Time `json:"end" yaml:"end"`
|
||||
RewardsPerSecond sdk.Coin `json:"rewards_per_second" yaml:"rewards_per_second"`
|
||||
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
|
||||
ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"`
|
||||
}
|
||||
|
||||
// NewDistributionSchedule returns a new DistributionSchedule
|
||||
func NewDistributionSchedule(active bool, denom string, start, end time.Time, reward sdk.Coin, claimEnd time.Time, multipliers Multipliers) DistributionSchedule {
|
||||
return DistributionSchedule{
|
||||
Active: active,
|
||||
DepositDenom: denom,
|
||||
Start: start,
|
||||
End: end,
|
||||
RewardsPerSecond: reward,
|
||||
ClaimEnd: claimEnd,
|
||||
ClaimMultipliers: multipliers,
|
||||
}
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer
|
||||
func (ds DistributionSchedule) String() string {
|
||||
return fmt.Sprintf(`Liquidity Provider Distribution Schedule:
|
||||
Deposit Denom: %s,
|
||||
Start: %s,
|
||||
End: %s,
|
||||
Rewards Per Second: %s,
|
||||
Claim End: %s,
|
||||
Active: %t
|
||||
`, ds.DepositDenom, ds.Start, ds.End, ds.RewardsPerSecond, ds.ClaimEnd, ds.Active)
|
||||
}
|
||||
|
||||
// Validate performs a basic check of a distribution schedule.
|
||||
func (ds DistributionSchedule) Validate() error {
|
||||
if !ds.RewardsPerSecond.IsValid() {
|
||||
return fmt.Errorf("invalid reward coins %s for %s", ds.RewardsPerSecond, ds.DepositDenom)
|
||||
}
|
||||
if !ds.RewardsPerSecond.IsPositive() {
|
||||
return fmt.Errorf("reward amount must be positive, is %s for %s", ds.RewardsPerSecond, ds.DepositDenom)
|
||||
}
|
||||
if ds.Start.IsZero() {
|
||||
return errors.New("reward period start time cannot be 0")
|
||||
}
|
||||
if ds.End.IsZero() {
|
||||
return errors.New("reward period end time cannot be 0")
|
||||
}
|
||||
if ds.Start.After(ds.End) {
|
||||
return fmt.Errorf("end period time %s cannot be before start time %s", ds.End, ds.Start)
|
||||
}
|
||||
if ds.ClaimEnd.Before(ds.End) {
|
||||
return fmt.Errorf("claim end time %s cannot be before end time %s", ds.ClaimEnd, ds.End)
|
||||
}
|
||||
for _, multiplier := range ds.ClaimMultipliers {
|
||||
if err := multiplier.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DistributionSchedules slice of DistributionSchedule
|
||||
type DistributionSchedules []DistributionSchedule
|
||||
|
||||
// Validate checks if all the LiquidityProviderSchedules are valid and there are no duplicated
|
||||
// entries.
|
||||
func (dss DistributionSchedules) Validate() error {
|
||||
seenPeriods := make(map[string]bool)
|
||||
for _, ds := range dss {
|
||||
if seenPeriods[ds.DepositDenom] {
|
||||
return fmt.Errorf("duplicated distribution provider schedule with deposit denom %s", ds.DepositDenom)
|
||||
}
|
||||
|
||||
if err := ds.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
seenPeriods[ds.DepositDenom] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer
|
||||
func (dss DistributionSchedules) String() string {
|
||||
out := "Distribution Schedules\n"
|
||||
for _, ds := range dss {
|
||||
out += fmt.Sprintf("%s\n", ds)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DelegatorDistributionSchedule distribution schedule for delegators
|
||||
type DelegatorDistributionSchedule struct {
|
||||
DistributionSchedule DistributionSchedule `json:"distribution_schedule" yaml:"distribution_schedule"`
|
||||
|
||||
DistributionFrequency time.Duration `json:"distribution_frequency" yaml:"distribution_frequency"`
|
||||
}
|
||||
|
||||
// NewDelegatorDistributionSchedule returns a new DelegatorDistributionSchedule
|
||||
func NewDelegatorDistributionSchedule(ds DistributionSchedule, frequency time.Duration) DelegatorDistributionSchedule {
|
||||
return DelegatorDistributionSchedule{
|
||||
DistributionSchedule: ds,
|
||||
DistributionFrequency: frequency,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs a basic check of a reward fields.
|
||||
func (dds DelegatorDistributionSchedule) Validate() error {
|
||||
if err := dds.DistributionSchedule.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if dds.DistributionFrequency <= 0 {
|
||||
return fmt.Errorf("distribution frequency should be positive, got %d", dds.DistributionFrequency)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelegatorDistributionSchedules slice of DelegatorDistributionSchedule
|
||||
type DelegatorDistributionSchedules []DelegatorDistributionSchedule
|
||||
|
||||
// Validate checks if all the LiquidityProviderSchedules are valid and there are no duplicated
|
||||
// entries.
|
||||
func (dds DelegatorDistributionSchedules) Validate() error {
|
||||
seenPeriods := make(map[string]bool)
|
||||
for _, ds := range dds {
|
||||
if seenPeriods[ds.DistributionSchedule.DepositDenom] {
|
||||
return fmt.Errorf("duplicated liquidity provider schedule with deposit denom %s", ds.DistributionSchedule.DepositDenom)
|
||||
}
|
||||
|
||||
if err := ds.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
seenPeriods[ds.DistributionSchedule.DepositDenom] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked
|
||||
type Multiplier struct {
|
||||
Name MultiplierName `json:"name" yaml:"name"`
|
||||
MonthsLockup int64 `json:"months_lockup" yaml:"months_lockup"`
|
||||
Factor sdk.Dec `json:"factor" yaml:"factor"`
|
||||
}
|
||||
|
||||
// NewMultiplier returns a new Multiplier
|
||||
func NewMultiplier(name MultiplierName, lockup int64, factor sdk.Dec) Multiplier {
|
||||
return Multiplier{
|
||||
Name: name,
|
||||
MonthsLockup: lockup,
|
||||
Factor: factor,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate multiplier param
|
||||
func (m Multiplier) Validate() error {
|
||||
if err := m.Name.IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.MonthsLockup < 0 {
|
||||
return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup)
|
||||
}
|
||||
if m.Factor.IsNegative() {
|
||||
return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMultiplier returns the named multiplier from the input distribution schedule
|
||||
func (ds DistributionSchedule) GetMultiplier(name MultiplierName) (Multiplier, bool) {
|
||||
for _, multiplier := range ds.ClaimMultipliers {
|
||||
if multiplier.Name == name {
|
||||
return multiplier, true
|
||||
}
|
||||
}
|
||||
return Multiplier{}, false
|
||||
}
|
||||
|
||||
// Multipliers slice of Multiplier
|
||||
type Multipliers []Multiplier
|
||||
|
||||
// NewParams returns a new params object
|
||||
func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules) Params {
|
||||
return Params{
|
||||
Active: active,
|
||||
LiquidityProviderSchedules: lps,
|
||||
DelegatorDistributionSchedules: dds,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultParams returns default params for harvest module
|
||||
func DefaultParams() Params {
|
||||
return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules)
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer
|
||||
func (p Params) String() string {
|
||||
return fmt.Sprintf(`Params:
|
||||
Active: %t
|
||||
Liquidity Provider Distribution Schedules %s
|
||||
Delegator Distribution Schedule %s`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules)
|
||||
}
|
||||
|
||||
// ParamKeyTable Key declaration for parameters
|
||||
func ParamKeyTable() params.KeyTable {
|
||||
return params.NewKeyTable().RegisterParamSet(&Params{})
|
||||
}
|
||||
|
||||
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
|
||||
func (p *Params) ParamSetPairs() params.ParamSetPairs {
|
||||
return params.ParamSetPairs{
|
||||
params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam),
|
||||
params.NewParamSetPair(KeyLPSchedules, &p.LiquidityProviderSchedules, validateLPParams),
|
||||
params.NewParamSetPair(KeyDelegatorSchedule, &p.DelegatorDistributionSchedules, validateDelegatorParams),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that the parameters have valid values.
|
||||
func (p Params) Validate() error {
|
||||
if err := validateActiveParam(p.Active); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateDelegatorParams(p.DelegatorDistributionSchedules); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validateLPParams(p.LiquidityProviderSchedules)
|
||||
}
|
||||
|
||||
func validateActiveParam(i interface{}) error {
|
||||
_, ok := i.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid parameter type: %T", i)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLPParams(i interface{}) error {
|
||||
dss, ok := i.(DistributionSchedules)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid parameter type: %T", i)
|
||||
}
|
||||
|
||||
for _, ds := range dss {
|
||||
err := ds.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDelegatorParams(i interface{}) error {
|
||||
dds, ok := i.(DelegatorDistributionSchedules)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid parameter type: %T", i)
|
||||
}
|
||||
|
||||
return dds.Validate()
|
||||
}
|
75
x/hvt/types/params_test.go
Normal file
75
x/hvt/types/params_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/hvt/types"
|
||||
)
|
||||
|
||||
type ParamTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ParamTestSuite) TestParamValidation() {
|
||||
type args struct {
|
||||
lps types.DistributionSchedules
|
||||
gds types.DistributionSchedules
|
||||
dds types.DelegatorDistributionSchedules
|
||||
active bool
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
expectPass bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
args: args{
|
||||
lps: types.DefaultLPSchedules,
|
||||
dds: types.DefaultDelegatorSchedules,
|
||||
active: types.DefaultActive,
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
lps: types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
dds: types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
expectPass: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
params := types.NewParams(tc.args.active, tc.args.lps, tc.args.dds)
|
||||
err := params.Validate()
|
||||
if tc.expectPass {
|
||||
suite.NoError(err)
|
||||
} else {
|
||||
suite.Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ParamTestSuite))
|
||||
}
|
20
x/hvt/types/period.go
Normal file
20
x/hvt/types/period.go
Normal file
@ -0,0 +1,20 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
|
||||
)
|
||||
|
||||
// NewPeriod returns a new vesting period
|
||||
func NewPeriod(amount sdk.Coins, length int64) vesting.Period {
|
||||
return vesting.Period{Amount: amount, Length: length}
|
||||
}
|
||||
|
||||
// GetTotalVestingPeriodLength returns the summed length of all vesting periods
|
||||
func GetTotalVestingPeriodLength(periods vesting.Periods) int64 {
|
||||
length := int64(0)
|
||||
for _, period := range periods {
|
||||
length += period.Length
|
||||
}
|
||||
return length
|
||||
}
|
69
x/hvt/types/querier.go
Normal file
69
x/hvt/types/querier.go
Normal file
@ -0,0 +1,69 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Querier routes for the harvest module
|
||||
const (
|
||||
QueryGetParams = "params"
|
||||
QueryGetModuleAccounts = "accounts"
|
||||
QueryGetDeposits = "deposits"
|
||||
QueryGetClaims = "claims"
|
||||
)
|
||||
|
||||
// QueryDepositParams is the params for a filtered deposit query
|
||||
type QueryDepositParams struct {
|
||||
Page int `json:"page" yaml:"page"`
|
||||
Limit int `json:"limit" yaml:"limit"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
|
||||
DepositType DepositType `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// NewQueryDepositParams creates a new QueryDepositParams
|
||||
func NewQueryDepositParams(page, limit int, depositDenom string, owner sdk.AccAddress, depositType DepositType) QueryDepositParams {
|
||||
return QueryDepositParams{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
DepositDenom: depositDenom,
|
||||
Owner: owner,
|
||||
DepositType: depositType,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryClaimParams is the params for a filtered claim query
|
||||
type QueryClaimParams struct {
|
||||
Page int `json:"page" yaml:"page"`
|
||||
Limit int `json:"limit" yaml:"limit"`
|
||||
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
|
||||
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
|
||||
DepositType DepositType `json:"deposit_type" yaml:"deposit_type"`
|
||||
}
|
||||
|
||||
// NewQueryClaimParams creates a new QueryClaimParams
|
||||
func NewQueryClaimParams(page, limit int, depositDenom string, owner sdk.AccAddress, depositType DepositType) QueryClaimParams {
|
||||
return QueryClaimParams{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
DepositDenom: depositDenom,
|
||||
Owner: owner,
|
||||
DepositType: depositType,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryAccountParams is the params for a filtered module account query
|
||||
type QueryAccountParams struct {
|
||||
Page int `json:"page" yaml:"page"`
|
||||
Limit int `json:"limit" yaml:"limit"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
}
|
||||
|
||||
// NewQueryAccountParams returns QueryAccountParams
|
||||
func NewQueryAccountParams(page, limit int, name string) QueryAccountParams {
|
||||
return QueryAccountParams{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Name: name,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user