mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 23:35:19 +00:00
Add Combined Earn and Liquid msgs (#1305)
* add new msg type definitions * add msg methods and tests * add module and keeper skeleton * add deposit and withdraw methods (no delegation) * untested depsit/withdraw with delegation methods * add cli cmds * fix cli argument parsing * add tests for delegate/undelegate msgs * emit un/delegate events * add godoc comments
This commit is contained in:
parent
314f733cb8
commit
9519690324
14
app/app.go
14
app/app.go
@ -131,6 +131,9 @@ import (
|
||||
pricefeed "github.com/kava-labs/kava/x/pricefeed"
|
||||
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
|
||||
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
|
||||
"github.com/kava-labs/kava/x/router"
|
||||
routerkeeper "github.com/kava-labs/kava/x/router/keeper"
|
||||
routertypes "github.com/kava-labs/kava/x/router/types"
|
||||
savings "github.com/kava-labs/kava/x/savings"
|
||||
savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
|
||||
savingstypes "github.com/kava-labs/kava/x/savings/types"
|
||||
@ -195,6 +198,7 @@ var (
|
||||
evmutil.AppModuleBasic{},
|
||||
liquid.AppModuleBasic{},
|
||||
earn.AppModuleBasic{},
|
||||
router.AppModuleBasic{},
|
||||
)
|
||||
|
||||
// module account permissions
|
||||
@ -291,6 +295,7 @@ type App struct {
|
||||
savingsKeeper savingskeeper.Keeper
|
||||
liquidKeeper liquidkeeper.Keeper
|
||||
earnKeeper earnkeeper.Keeper
|
||||
routerKeeper routerkeeper.Keeper
|
||||
|
||||
// make scoped keepers public for test purposes
|
||||
ScopedIBCKeeper capabilitykeeper.ScopedKeeper
|
||||
@ -624,6 +629,11 @@ func NewApp(
|
||||
nil,
|
||||
&earnKeeper,
|
||||
)
|
||||
app.routerKeeper = routerkeeper.NewKeeper(
|
||||
&app.earnKeeper,
|
||||
app.liquidKeeper,
|
||||
&app.stakingKeeper,
|
||||
)
|
||||
|
||||
// create committee keeper with router
|
||||
committeeGovRouter := govtypes.NewRouter()
|
||||
@ -718,6 +728,7 @@ func NewApp(
|
||||
savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper),
|
||||
liquid.NewAppModule(app.liquidKeeper),
|
||||
earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper),
|
||||
router.NewAppModule(app.routerKeeper),
|
||||
)
|
||||
|
||||
// Warning: Some begin blockers must run before others. Ensure the dependencies are understood before modifying this list.
|
||||
@ -766,6 +777,7 @@ func NewApp(
|
||||
savingstypes.ModuleName,
|
||||
liquidtypes.ModuleName,
|
||||
earntypes.ModuleName,
|
||||
routertypes.ModuleName,
|
||||
)
|
||||
|
||||
// Warning: Some end blockers must run before others. Ensure the dependencies are understood before modifying this list.
|
||||
@ -806,6 +818,7 @@ func NewApp(
|
||||
savingstypes.ModuleName,
|
||||
liquidtypes.ModuleName,
|
||||
earntypes.ModuleName,
|
||||
routertypes.ModuleName,
|
||||
)
|
||||
|
||||
// Warning: Some init genesis methods must run before others. Ensure the dependencies are understood before modifying this list
|
||||
@ -845,6 +858,7 @@ func NewApp(
|
||||
upgradetypes.ModuleName,
|
||||
validatorvestingtypes.ModuleName,
|
||||
liquidtypes.ModuleName,
|
||||
routertypes.ModuleName,
|
||||
)
|
||||
|
||||
app.mm.RegisterInvariants(&app.crisisKeeper)
|
||||
|
@ -42,6 +42,7 @@ import (
|
||||
kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
|
||||
liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper"
|
||||
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
|
||||
routerkeeper "github.com/kava-labs/kava/x/router/keeper"
|
||||
savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
|
||||
swapkeeper "github.com/kava-labs/kava/x/swap/keeper"
|
||||
)
|
||||
@ -112,6 +113,7 @@ func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper { return tApp.sa
|
||||
func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper }
|
||||
func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper { return tApp.liquidKeeper }
|
||||
func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper }
|
||||
func (tApp TestApp) GetRouterKeeper() routerkeeper.Keeper { return tApp.routerKeeper }
|
||||
|
||||
func (tApp TestApp) GetStoreKey(s string) sdk.StoreKey { return tApp.keys[s] }
|
||||
|
||||
|
@ -3,6 +3,7 @@ set -e
|
||||
|
||||
validatorMnemonic="equip town gesture square tomorrow volume nephew minute witness beef rich gadget actress egg sing secret pole winter alarm law today check violin uncover"
|
||||
# kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
|
||||
# kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
|
||||
|
||||
faucetMnemonic="crash sort dwarf disease change advice attract clump avoid mobile clump right junior axis book fresh mask tube front require until face effort vault"
|
||||
# kava1adkm6svtzjsxxvg7g6rshg6kj9qwej8gwqadqd
|
||||
@ -92,5 +93,14 @@ jq '.app_state.evm.params.chain_config.merge_fork_block = null' $DATA/config/gen
|
||||
jq '.app_state.earn.params.allowed_vaults = [
|
||||
{
|
||||
denom: "usdx",
|
||||
vault_strategy: 1,
|
||||
strategies: ["STRATEGY_TYPE_HARD"],
|
||||
},
|
||||
{
|
||||
denom: "bkava",
|
||||
strategies: ["STRATEGY_TYPE_SAVINGS"],
|
||||
}]' $DATA/config/genesis.json | sponge $DATA/config/genesis.json
|
||||
|
||||
jq '.app_state.savings.params.supported_denoms = ["bkava-kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0"]' $DATA/config/genesis.json | sponge $DATA/config/genesis.json
|
||||
|
||||
|
||||
$BINARY config broadcast-mode block
|
@ -430,6 +430,18 @@
|
||||
|
||||
- [Msg](#kava.pricefeed.v1beta1.Msg)
|
||||
|
||||
- [kava/router/v1beta1/tx.proto](#kava/router/v1beta1/tx.proto)
|
||||
- [MsgDelegateMintDeposit](#kava.router.v1beta1.MsgDelegateMintDeposit)
|
||||
- [MsgDelegateMintDepositResponse](#kava.router.v1beta1.MsgDelegateMintDepositResponse)
|
||||
- [MsgMintDeposit](#kava.router.v1beta1.MsgMintDeposit)
|
||||
- [MsgMintDepositResponse](#kava.router.v1beta1.MsgMintDepositResponse)
|
||||
- [MsgWithdrawBurn](#kava.router.v1beta1.MsgWithdrawBurn)
|
||||
- [MsgWithdrawBurnResponse](#kava.router.v1beta1.MsgWithdrawBurnResponse)
|
||||
- [MsgWithdrawBurnUndelegate](#kava.router.v1beta1.MsgWithdrawBurnUndelegate)
|
||||
- [MsgWithdrawBurnUndelegateResponse](#kava.router.v1beta1.MsgWithdrawBurnUndelegateResponse)
|
||||
|
||||
- [Msg](#kava.router.v1beta1.Msg)
|
||||
|
||||
- [kava/savings/v1beta1/store.proto](#kava/savings/v1beta1/store.proto)
|
||||
- [Deposit](#kava.savings.v1beta1.Deposit)
|
||||
- [Params](#kava.savings.v1beta1.Params)
|
||||
@ -5957,6 +5969,145 @@ Msg defines the pricefeed Msg service.
|
||||
|
||||
|
||||
|
||||
<a name="kava/router/v1beta1/tx.proto"></a>
|
||||
<p align="right"><a href="#top">Top</a></p>
|
||||
|
||||
## kava/router/v1beta1/tx.proto
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgDelegateMintDeposit"></a>
|
||||
|
||||
### MsgDelegateMintDeposit
|
||||
MsgDelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
|
||||
then deposits to an earn vault.
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| `depositor` | [string](#string) | | depositor represents the owner of the tokens to delegate |
|
||||
| `validator` | [string](#string) | | validator is the address of the validator to delegate to |
|
||||
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the tokens to delegate |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgDelegateMintDepositResponse"></a>
|
||||
|
||||
### MsgDelegateMintDepositResponse
|
||||
MsgDelegateMintDepositResponse defines the Msg/MsgDelegateMintDeposit response type.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgMintDeposit"></a>
|
||||
|
||||
### MsgMintDeposit
|
||||
MsgMintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault.
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| `depositor` | [string](#string) | | depositor represents the owner of the delegation to convert |
|
||||
| `validator` | [string](#string) | | validator is the validator for the depositor's delegation |
|
||||
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the delegation balance to convert |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgMintDepositResponse"></a>
|
||||
|
||||
### MsgMintDepositResponse
|
||||
MsgMintDepositResponse defines the Msg/MsgMintDeposit response type.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgWithdrawBurn"></a>
|
||||
|
||||
### MsgWithdrawBurn
|
||||
MsgWithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| `from` | [string](#string) | | from is the owner of the earn vault to withdraw from |
|
||||
| `validator` | [string](#string) | | validator is the address to select the derivative denom to withdraw |
|
||||
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the staked token equivalent to withdraw |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgWithdrawBurnResponse"></a>
|
||||
|
||||
### MsgWithdrawBurnResponse
|
||||
MsgWithdrawBurnResponse defines the Msg/MsgWithdrawBurn response type.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgWithdrawBurnUndelegate"></a>
|
||||
|
||||
### MsgWithdrawBurnUndelegate
|
||||
MsgWithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
|
||||
then undelegates them from their validator.
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| `from` | [string](#string) | | from is the owner of the earn vault to withdraw from |
|
||||
| `validator` | [string](#string) | | validator is the address to select the derivative denom to withdraw |
|
||||
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the staked token equivalent to withdraw |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.MsgWithdrawBurnUndelegateResponse"></a>
|
||||
|
||||
### MsgWithdrawBurnUndelegateResponse
|
||||
MsgWithdrawBurnUndelegateResponse defines the Msg/MsgWithdrawBurnUndelegate response type.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- end messages -->
|
||||
|
||||
<!-- end enums -->
|
||||
|
||||
<!-- end HasExtensions -->
|
||||
|
||||
|
||||
<a name="kava.router.v1beta1.Msg"></a>
|
||||
|
||||
### Msg
|
||||
Msg defines the router Msg service.
|
||||
|
||||
| Method Name | Request Type | Response Type | Description | HTTP Verb | Endpoint |
|
||||
| ----------- | ------------ | ------------- | ------------| ------- | -------- |
|
||||
| `MintDeposit` | [MsgMintDeposit](#kava.router.v1beta1.MsgMintDeposit) | [MsgMintDepositResponse](#kava.router.v1beta1.MsgMintDepositResponse) | MintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault. | |
|
||||
| `DelegateMintDeposit` | [MsgDelegateMintDeposit](#kava.router.v1beta1.MsgDelegateMintDeposit) | [MsgDelegateMintDepositResponse](#kava.router.v1beta1.MsgDelegateMintDepositResponse) | DelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives, then deposits to an earn vault. | |
|
||||
| `WithdrawBurn` | [MsgWithdrawBurn](#kava.router.v1beta1.MsgWithdrawBurn) | [MsgWithdrawBurnResponse](#kava.router.v1beta1.MsgWithdrawBurnResponse) | WithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation. | |
|
||||
| `WithdrawBurnUndelegate` | [MsgWithdrawBurnUndelegate](#kava.router.v1beta1.MsgWithdrawBurnUndelegate) | [MsgWithdrawBurnUndelegateResponse](#kava.router.v1beta1.MsgWithdrawBurnUndelegateResponse) | WithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation, then undelegates them from their validator. | |
|
||||
|
||||
<!-- end services -->
|
||||
|
||||
|
||||
|
||||
<a name="kava/savings/v1beta1/store.proto"></a>
|
||||
<p align="right"><a href="#top">Top</a></p>
|
||||
|
||||
|
76
proto/kava/router/v1beta1/tx.proto
Normal file
76
proto/kava/router/v1beta1/tx.proto
Normal file
@ -0,0 +1,76 @@
|
||||
syntax = "proto3";
|
||||
package kava.router.v1beta1;
|
||||
|
||||
import "cosmos_proto/cosmos.proto";
|
||||
import "cosmos/base/v1beta1/coin.proto";
|
||||
import "gogoproto/gogo.proto";
|
||||
|
||||
option go_package = "github.com/kava-labs/kava/x/router/types";
|
||||
option (gogoproto.goproto_getters_all) = false;
|
||||
|
||||
// Msg defines the router Msg service.
|
||||
service Msg {
|
||||
// MintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault.
|
||||
rpc MintDeposit(MsgMintDeposit) returns (MsgMintDepositResponse);
|
||||
|
||||
// DelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
|
||||
// then deposits to an earn vault.
|
||||
rpc DelegateMintDeposit(MsgDelegateMintDeposit) returns (MsgDelegateMintDepositResponse);
|
||||
|
||||
// WithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
|
||||
rpc WithdrawBurn(MsgWithdrawBurn) returns (MsgWithdrawBurnResponse);
|
||||
|
||||
// WithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
|
||||
// then undelegates them from their validator.
|
||||
rpc WithdrawBurnUndelegate(MsgWithdrawBurnUndelegate) returns (MsgWithdrawBurnUndelegateResponse);
|
||||
}
|
||||
|
||||
// MsgMintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault.
|
||||
message MsgMintDeposit {
|
||||
// depositor represents the owner of the delegation to convert
|
||||
string depositor = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
|
||||
// validator is the validator for the depositor's delegation
|
||||
string validator = 2;
|
||||
// amount is the delegation balance to convert
|
||||
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
|
||||
}
|
||||
// MsgMintDepositResponse defines the Msg/MsgMintDeposit response type.
|
||||
message MsgMintDepositResponse {}
|
||||
|
||||
// MsgDelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
|
||||
// then deposits to an earn vault.
|
||||
message MsgDelegateMintDeposit {
|
||||
// depositor represents the owner of the tokens to delegate
|
||||
string depositor = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
|
||||
// validator is the address of the validator to delegate to
|
||||
string validator = 2;
|
||||
// amount is the tokens to delegate
|
||||
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
|
||||
}
|
||||
// MsgDelegateMintDepositResponse defines the Msg/MsgDelegateMintDeposit response type.
|
||||
message MsgDelegateMintDepositResponse {}
|
||||
|
||||
// MsgWithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
|
||||
message MsgWithdrawBurn {
|
||||
// from is the owner of the earn vault to withdraw from
|
||||
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
|
||||
// validator is the address to select the derivative denom to withdraw
|
||||
string validator = 2;
|
||||
// amount is the staked token equivalent to withdraw
|
||||
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
|
||||
}
|
||||
// MsgWithdrawBurnResponse defines the Msg/MsgWithdrawBurn response type.
|
||||
message MsgWithdrawBurnResponse {}
|
||||
|
||||
// MsgWithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
|
||||
// then undelegates them from their validator.
|
||||
message MsgWithdrawBurnUndelegate {
|
||||
// from is the owner of the earn vault to withdraw from
|
||||
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
|
||||
// validator is the address to select the derivative denom to withdraw
|
||||
string validator = 2;
|
||||
// amount is the staked token equivalent to withdraw
|
||||
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
|
||||
}
|
||||
// MsgWithdrawBurnUndelegateResponse defines the Msg/MsgWithdrawBurnUndelegate response type.
|
||||
message MsgWithdrawBurnUndelegateResponse {}
|
@ -155,3 +155,21 @@ func (k Keeper) burnCoins(ctx sdk.Context, sender sdk.AccAddress, amount sdk.Coi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DerivativeFromTokens calculates the approximate amount of derivative coins that would be minted for a given amount of staking tokens.
|
||||
func (k Keeper) DerivativeFromTokens(ctx sdk.Context, valAddr sdk.ValAddress, tokens sdk.Coin) (sdk.Coin, error) {
|
||||
bondDenom := k.stakingKeeper.BondDenom(ctx)
|
||||
if tokens.Denom != bondDenom {
|
||||
return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidDenom, "'%s' does not match staking denom '%s'", tokens.Denom, bondDenom)
|
||||
}
|
||||
|
||||
// Use GetModuleAddress instead of GetModuleAccount to avoid creating a module account if it doesn't exist.
|
||||
modAddress := k.accountKeeper.GetModuleAddress(types.ModuleAccountName)
|
||||
derivative, _, err := k.CalculateDerivativeSharesFromTokens(ctx, modAddress, valAddr, tokens.Amount)
|
||||
if err != nil {
|
||||
return sdk.Coin{}, err
|
||||
}
|
||||
liquidTokenDenom := k.GetLiquidStakingTokenDenom(valAddr)
|
||||
liquidToken := sdk.NewCoin(liquidTokenDenom, derivative)
|
||||
return liquidToken, nil
|
||||
}
|
||||
|
@ -469,3 +469,27 @@ func (suite *KeeperTestSuite) TestGetStakedTokensForDerivatives() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestDerivativeFromTokens() {
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(1)
|
||||
valAccAddr := addrs[0]
|
||||
valAddr := sdk.ValAddress(valAccAddr)
|
||||
moduleAccAddress := authtypes.NewModuleAddress(types.ModuleAccountName)
|
||||
|
||||
initialBalance := i(1e9)
|
||||
|
||||
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(initialBalance))
|
||||
suite.AddCoinsToModule(types.ModuleAccountName, suite.NewBondCoins(initialBalance))
|
||||
|
||||
suite.CreateNewUnbondedValidator(valAddr, initialBalance)
|
||||
suite.CreateDelegation(valAddr, moduleAccAddress, initialBalance)
|
||||
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||
|
||||
_, err := suite.Keeper.DerivativeFromTokens(suite.Ctx, valAddr, sdk.NewCoin("invalid", initialBalance))
|
||||
suite.ErrorIs(err, types.ErrInvalidDenom)
|
||||
|
||||
derivatives, err := suite.Keeper.DerivativeFromTokens(suite.Ctx, valAddr, suite.NewBondCoin(initialBalance))
|
||||
suite.NoError(err)
|
||||
expected := sdk.NewCoin(fmt.Sprintf("bkava-%s", valAddr), initialBalance)
|
||||
suite.Equal(expected, derivatives)
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ type BankKeeper interface {
|
||||
|
||||
// AccountKeeper defines the expected keeper interface for interacting with account
|
||||
type AccountKeeper interface {
|
||||
GetModuleAddress(moduleName string) sdk.AccAddress
|
||||
GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI
|
||||
}
|
||||
|
||||
|
174
x/router/client/cli/tx.go
Normal file
174
x/router/client/cli/tx.go
Normal file
@ -0,0 +1,174 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
|
||||
"github.com/kava-labs/kava/x/router/types"
|
||||
)
|
||||
|
||||
// GetTxCmd returns the transaction commands for this module
|
||||
func GetTxCmd() *cobra.Command {
|
||||
liquidTxCmd := &cobra.Command{
|
||||
Use: types.ModuleName,
|
||||
Short: "router transactions subcommands",
|
||||
DisableFlagParsing: true,
|
||||
SuggestionsMinimumDistance: 2,
|
||||
RunE: client.ValidateCmd,
|
||||
}
|
||||
|
||||
cmds := []*cobra.Command{
|
||||
getCmdMintDeposit(),
|
||||
getCmdDelegateMintDeposit(),
|
||||
getCmdWithdrawBurn(),
|
||||
getCmdWithdrawBurnUndelegate(),
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
flags.AddTxFlagsToCmd(cmd)
|
||||
}
|
||||
|
||||
liquidTxCmd.AddCommand(cmds...)
|
||||
|
||||
return liquidTxCmd
|
||||
}
|
||||
|
||||
func getCmdMintDeposit() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "mint-deposit [validator-addr] [amount]",
|
||||
Short: "mints staking derivative from a delegation and deposits them to earn",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s mint-deposit kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valAddr, err := sdk.ValAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
|
||||
}
|
||||
|
||||
coin, err := sdk.ParseCoinNormalized(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.NewMsgMintDeposit(clientCtx.GetFromAddress(), valAddr, coin)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCmdDelegateMintDeposit() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "delegate-mint-deposit [validator-addr] [amount]",
|
||||
Short: "delegates tokens, mints staking derivative from a them, and deposits them to earn",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s delegate-mint-deposit kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valAddr, err := sdk.ValAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
|
||||
}
|
||||
|
||||
coin, err := sdk.ParseCoinNormalized(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.NewMsgDelegateMintDeposit(clientCtx.GetFromAddress(), valAddr, coin)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCmdWithdrawBurn() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "withdraw-burn [validator-addr] [amount]",
|
||||
Short: "withdraws staking derivatives from earn and burns them to redeem a delegation",
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s withdraw-burn kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
|
||||
),
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valAddr, err := sdk.ValAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
|
||||
}
|
||||
|
||||
amount, err := sdk.ParseCoinNormalized(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.NewMsgWithdrawBurn(clientCtx.GetFromAddress(), valAddr, amount)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getCmdWithdrawBurnUndelegate() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "withdraw-burn-undelegate [validator-addr] [amount]",
|
||||
Short: "withdraws staking derivatives from earn, burns them to redeem a delegation, then unstakes the delegation",
|
||||
Example: fmt.Sprintf(
|
||||
`%s tx %s withdraw-burn-undelegate kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
|
||||
),
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valAddr, err := sdk.ValAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
|
||||
}
|
||||
|
||||
amount, err := sdk.ParseCoinNormalized(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.NewMsgWithdrawBurnUndelegate(clientCtx.GetFromAddress(), valAddr, amount)
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
|
||||
},
|
||||
}
|
||||
}
|
26
x/router/keeper/keeper.go
Normal file
26
x/router/keeper/keeper.go
Normal file
@ -0,0 +1,26 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"github.com/kava-labs/kava/x/router/types"
|
||||
)
|
||||
|
||||
// Keeper is the keeper for the module
|
||||
type Keeper struct {
|
||||
earnKeeper types.EarnKeeper
|
||||
liquidKeeper types.LiquidKeeper
|
||||
stakingKeeper types.StakingKeeper
|
||||
}
|
||||
|
||||
// NewKeeper creates a new keeper
|
||||
func NewKeeper(
|
||||
earnKeeper types.EarnKeeper,
|
||||
liquidKeeper types.LiquidKeeper,
|
||||
stakingKeeper types.StakingKeeper,
|
||||
) Keeper {
|
||||
|
||||
return Keeper{
|
||||
earnKeeper: earnKeeper,
|
||||
liquidKeeper: liquidKeeper,
|
||||
stakingKeeper: stakingKeeper,
|
||||
}
|
||||
}
|
201
x/router/keeper/msg_server.go
Normal file
201
x/router/keeper/msg_server.go
Normal file
@ -0,0 +1,201 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
|
||||
earntypes "github.com/kava-labs/kava/x/earn/types"
|
||||
"github.com/kava-labs/kava/x/router/types"
|
||||
)
|
||||
|
||||
type msgServer struct {
|
||||
keeper Keeper
|
||||
}
|
||||
|
||||
// NewMsgServerImpl returns an implementation of the module's MsgServer interface
|
||||
// for the provided Keeper.
|
||||
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
|
||||
return &msgServer{keeper: keeper}
|
||||
}
|
||||
|
||||
var _ types.MsgServer = msgServer{}
|
||||
|
||||
// MintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault
|
||||
func (m msgServer) MintDeposit(goCtx context.Context, msg *types.MsgMintDeposit) (*types.MsgMintDepositResponse, error) {
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
|
||||
depositor, err := sdk.AccAddressFromBech32(msg.Depositor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := sdk.ValAddressFromBech32(msg.Validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derivative, err := m.keeper.liquidKeeper.MintDerivative(ctx, depositor, val, msg.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.keeper.earnKeeper.Deposit(ctx, depositor, derivative, earntypes.STRATEGY_TYPE_SAVINGS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
|
||||
),
|
||||
)
|
||||
return &types.MsgMintDepositResponse{}, nil
|
||||
}
|
||||
|
||||
// DelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
|
||||
// then deposits to an earn vault.
|
||||
func (m msgServer) DelegateMintDeposit(goCtx context.Context, msg *types.MsgDelegateMintDeposit) (*types.MsgDelegateMintDepositResponse, error) {
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
|
||||
depositor, err := sdk.AccAddressFromBech32(msg.Depositor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valAddr, err := sdk.ValAddressFromBech32(msg.Validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validator, found := m.keeper.stakingKeeper.GetValidator(ctx, valAddr)
|
||||
if !found {
|
||||
return nil, stakingtypes.ErrNoValidatorFound
|
||||
}
|
||||
bondDenom := m.keeper.stakingKeeper.BondDenom(ctx)
|
||||
if msg.Amount.Denom != bondDenom {
|
||||
return nil, sdkerrors.Wrapf(
|
||||
sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Amount.Denom, bondDenom,
|
||||
)
|
||||
}
|
||||
newShares, err := m.keeper.stakingKeeper.Delegate(ctx, depositor, msg.Amount.Amount, stakingtypes.Unbonded, validator, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derivativeMinted, err := m.keeper.liquidKeeper.MintDerivative(ctx, depositor, valAddr, msg.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.keeper.earnKeeper.Deposit(ctx, depositor, derivativeMinted, earntypes.STRATEGY_TYPE_SAVINGS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvents(sdk.Events{
|
||||
sdk.NewEvent(
|
||||
stakingtypes.EventTypeDelegate,
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, valAddr.String()),
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyNewShares, newShares.String()),
|
||||
),
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
|
||||
),
|
||||
})
|
||||
|
||||
return &types.MsgDelegateMintDepositResponse{}, nil
|
||||
}
|
||||
|
||||
// WithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
|
||||
func (m msgServer) WithdrawBurn(goCtx context.Context, msg *types.MsgWithdrawBurn) (*types.MsgWithdrawBurnResponse, error) {
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
|
||||
depositor, err := sdk.AccAddressFromBech32(msg.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := sdk.ValAddressFromBech32(msg.Validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenAmount, err := m.keeper.liquidKeeper.DerivativeFromTokens(ctx, val, msg.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, tokenAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
|
||||
),
|
||||
)
|
||||
|
||||
return &types.MsgWithdrawBurnResponse{}, nil
|
||||
}
|
||||
|
||||
// WithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
|
||||
// then undelegates them from their validator.
|
||||
func (m msgServer) WithdrawBurnUndelegate(goCtx context.Context, msg *types.MsgWithdrawBurnUndelegate) (*types.MsgWithdrawBurnUndelegateResponse, error) {
|
||||
ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
|
||||
depositor, err := sdk.AccAddressFromBech32(msg.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := sdk.ValAddressFromBech32(msg.Validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenAmount, err := m.keeper.liquidKeeper.DerivativeFromTokens(ctx, val, msg.Amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sharesReturned, err := m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, tokenAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
completionTime, err := m.keeper.stakingKeeper.Undelegate(ctx, depositor, val, sharesReturned)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvents(sdk.Events{
|
||||
sdk.NewEvent(
|
||||
stakingtypes.EventTypeUnbond,
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, val.String()),
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)),
|
||||
),
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
|
||||
),
|
||||
})
|
||||
return &types.MsgWithdrawBurnUndelegateResponse{}, nil
|
||||
}
|
321
x/router/keeper/msg_server_test.go
Normal file
321
x/router/keeper/msg_server_test.go
Normal file
@ -0,0 +1,321 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/staking"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
earntypes "github.com/kava-labs/kava/x/earn/types"
|
||||
"github.com/kava-labs/kava/x/router/keeper"
|
||||
"github.com/kava-labs/kava/x/router/testutil"
|
||||
"github.com/kava-labs/kava/x/router/types"
|
||||
)
|
||||
|
||||
type msgServerTestSuite struct {
|
||||
testutil.Suite
|
||||
|
||||
msgServer types.MsgServer
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) SetupTest() {
|
||||
suite.Suite.SetupTest()
|
||||
|
||||
suite.msgServer = keeper.NewMsgServerImpl(suite.Keeper)
|
||||
}
|
||||
|
||||
func TestMsgServerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(msgServerTestSuite))
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) TestMintDeposit_Events() {
|
||||
user, valAddr, delegation := suite.setupValidatorAndDelegation()
|
||||
suite.setupEarnForDeposits(valAddr)
|
||||
|
||||
msg := types.NewMsgMintDeposit(
|
||||
user,
|
||||
valAddr,
|
||||
suite.NewBondCoin(delegation),
|
||||
)
|
||||
_, err := suite.msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.EventsContains(suite.Ctx.EventManager().Events(),
|
||||
sdk.NewEvent(sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) TestDelegateMintDeposit_Events() {
|
||||
user, valAddr, balance := suite.setupValidator()
|
||||
suite.setupEarnForDeposits(valAddr)
|
||||
|
||||
msg := types.NewMsgDelegateMintDeposit(
|
||||
user,
|
||||
valAddr,
|
||||
suite.NewBondCoin(balance),
|
||||
)
|
||||
_, err := suite.msgServer.DelegateMintDeposit(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.EventsContains(suite.Ctx.EventManager().Events(),
|
||||
sdk.NewEvent(sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
|
||||
),
|
||||
)
|
||||
expectedShares := msg.Amount.Amount.ToDec() // no slashes so shares equal staked tokens
|
||||
suite.EventsContains(suite.Ctx.EventManager().Events(),
|
||||
sdk.NewEvent(
|
||||
stakingtypes.EventTypeDelegate,
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, msg.Validator),
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyNewShares, expectedShares.String()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) TestWithdrawBurn_Events() {
|
||||
user, valAddr, delegated := suite.setupDerivatives()
|
||||
// clear events from setup
|
||||
suite.Ctx = suite.Ctx.WithEventManager(sdk.NewEventManager())
|
||||
|
||||
msg := types.NewMsgWithdrawBurn(
|
||||
user,
|
||||
valAddr,
|
||||
// in this setup where the validator is not slashed, the derivative amount is equal to the staked balance
|
||||
suite.NewBondCoin(delegated.Amount),
|
||||
)
|
||||
_, err := suite.msgServer.WithdrawBurn(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.EventsContains(suite.Ctx.EventManager().Events(),
|
||||
sdk.NewEvent(sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) TestWithdrawBurnUndelegate_Events() {
|
||||
user, valAddr, delegated := suite.setupDerivatives()
|
||||
// clear events from setup
|
||||
suite.Ctx = suite.Ctx.WithEventManager(sdk.NewEventManager())
|
||||
|
||||
msg := types.NewMsgWithdrawBurnUndelegate(
|
||||
user,
|
||||
valAddr,
|
||||
// in this setup where the validator is not slashed, the derivative amount is equal to the staked balance
|
||||
suite.NewBondCoin(delegated.Amount),
|
||||
)
|
||||
_, err := suite.msgServer.WithdrawBurnUndelegate(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.EventsContains(suite.Ctx.EventManager().Events(),
|
||||
sdk.NewEvent(sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
|
||||
),
|
||||
)
|
||||
unbondingTime := suite.StakingKeeper.UnbondingTime(suite.Ctx)
|
||||
completionTime := suite.Ctx.BlockTime().Add(unbondingTime)
|
||||
suite.EventsContains(suite.Ctx.EventManager().Events(),
|
||||
sdk.NewEvent(
|
||||
stakingtypes.EventTypeUnbond,
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, msg.Validator),
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
|
||||
sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) TestMintDepositAndWithdrawBurn_TransferEntireBalance() {
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(5)
|
||||
valAccAddr, user := addrs[0], addrs[1]
|
||||
valAddr := sdk.ValAddress(valAccAddr)
|
||||
|
||||
derivativeDenom := suite.setupEarnForDeposits(valAddr)
|
||||
|
||||
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(sdk.NewInt(1e9)))
|
||||
vesting := sdk.NewInt(1000)
|
||||
suite.CreateVestingAccountWithAddress(user,
|
||||
suite.NewBondCoins(sdk.NewInt(1e9).Add(vesting)),
|
||||
suite.NewBondCoins(vesting),
|
||||
)
|
||||
|
||||
// Create a slashed validator, where the delegator owns fractional tokens.
|
||||
suite.CreateNewUnbondedValidator(valAddr, sdk.NewInt(1e9))
|
||||
suite.CreateDelegation(valAddr, user, sdk.NewInt(1e9))
|
||||
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||
suite.SlashValidator(valAddr, sdk.MustNewDecFromStr("0.666666666666666667"))
|
||||
|
||||
// Query the full staked balance and convert it all to derivatives
|
||||
// user technically 333_333_333.333333333333333333 staked tokens without rounding
|
||||
delegation := suite.QueryStaking_Delegation(valAddr, user)
|
||||
suite.Equal(sdk.NewInt(333_333_333), delegation.Balance.Amount)
|
||||
|
||||
msgDeposit := types.NewMsgMintDeposit(
|
||||
user,
|
||||
valAddr,
|
||||
delegation.Balance,
|
||||
)
|
||||
_, err := suite.msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), msgDeposit)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// There should be no extractable balance left in delegation
|
||||
suite.DelegationBalanceLessThan(valAddr, user, sdk.NewInt(1))
|
||||
// All derivative coins should be deposited to earn
|
||||
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
|
||||
// Earn vault has all minted derivatives
|
||||
suite.VaultAccountValueEqual(user, sdk.NewInt64Coin(derivativeDenom, 999_999_998)) // 2 lost in conversion
|
||||
|
||||
// Query the full kava balance of the earn deposit and convert all to a delegation
|
||||
deposit := suite.QueryEarn_VaultValue(user, "bkava")
|
||||
suite.Equal(suite.NewBondCoins(sdk.NewInt(333_333_332)), deposit.Value) // 1 lost due to lost shares
|
||||
|
||||
msgWithdraw := types.NewMsgWithdrawBurn(
|
||||
user,
|
||||
valAddr,
|
||||
deposit.Value[0],
|
||||
)
|
||||
_, err = suite.msgServer.WithdrawBurn(sdk.WrapSDKContext(suite.Ctx), msgWithdraw)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// There should be no earn deposit left (earn removes dust amounts)
|
||||
suite.VaultAccountSharesEqual(user, nil)
|
||||
// All derivative coins should be converted to a delegation
|
||||
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
|
||||
// The user should get back most of their original deposited balance
|
||||
suite.DelegationBalanceInDeltaBelow(valAddr, user, sdk.NewInt(333_333_332), sdk.NewInt(2))
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) TestDelegateMintDepositAndWithdrawBurnUndelegate_TransferEntireBalance() {
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(5)
|
||||
valAccAddr, user := addrs[0], addrs[1]
|
||||
valAddr := sdk.ValAddress(valAccAddr)
|
||||
|
||||
derivativeDenom := suite.setupEarnForDeposits(valAddr)
|
||||
|
||||
valBalance := sdk.NewInt(1e9)
|
||||
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(valBalance))
|
||||
|
||||
// Create a slashed validator, where a future delegator will own fractional tokens.
|
||||
suite.CreateNewUnbondedValidator(valAddr, valBalance)
|
||||
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||
suite.SlashValidator(valAddr, sdk.MustNewDecFromStr("0.4")) // tokens remaining 600_000_000
|
||||
|
||||
userBalance := sdk.NewInt(100e6)
|
||||
vesting := sdk.NewInt(1000)
|
||||
suite.CreateVestingAccountWithAddress(user,
|
||||
suite.NewBondCoins(userBalance.Add(vesting)),
|
||||
suite.NewBondCoins(vesting),
|
||||
)
|
||||
|
||||
// Query the full vested balance and convert it all to derivatives
|
||||
balance := suite.QueryBank_SpendableBalance(user)
|
||||
suite.Equal(suite.NewBondCoins(userBalance), balance)
|
||||
|
||||
// When delegation is created it will have 166_666_666.666666666666666666 shares
|
||||
// newShares = validatorShares * newTokens/validatorTokens, truncated to 18 decimals
|
||||
msgDeposit := types.NewMsgDelegateMintDeposit(
|
||||
user,
|
||||
valAddr,
|
||||
balance[0],
|
||||
)
|
||||
_, err := suite.msgServer.DelegateMintDeposit(sdk.WrapSDKContext(suite.Ctx), msgDeposit)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// All spendable balance should be withdrawn
|
||||
suite.AccountSpendableBalanceEqual(user, nil)
|
||||
// Since shares are newly created, the exact amount should be converted to derivatives, leaving none behind
|
||||
suite.DelegationSharesEqual(valAddr, user, sdk.ZeroDec())
|
||||
// All derivative coins should be deposited to earn
|
||||
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
|
||||
|
||||
suite.VaultAccountValueEqual(user, sdk.NewInt64Coin(derivativeDenom, 166_666_666))
|
||||
|
||||
// Query the full kava balance of the earn deposit and convert all to a delegation
|
||||
deposit := suite.QueryEarn_VaultValue(user, "bkava")
|
||||
suite.Equal(suite.NewBondCoins(sdk.NewInt(99_999_999)), deposit.Value) // 1 lost due to truncating shares to derivatives
|
||||
|
||||
msgWithdraw := types.NewMsgWithdrawBurnUndelegate(
|
||||
user,
|
||||
valAddr,
|
||||
deposit.Value[0],
|
||||
)
|
||||
_, err = suite.msgServer.WithdrawBurnUndelegate(sdk.WrapSDKContext(suite.Ctx), msgWithdraw)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// There should be no earn deposit left (earn removes dust amounts)
|
||||
suite.VaultAccountSharesEqual(user, nil)
|
||||
// All derivative coins should be converted to a delegation
|
||||
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
|
||||
// There should be zero shares left because undelegate removes all created by burn.
|
||||
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
|
||||
// User should have most of their original balance back in an unbonding delegation
|
||||
suite.UnbondingDelegationInDeltaBelow(valAddr, user, userBalance, sdk.NewInt(2))
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) setupValidator() (sdk.AccAddress, sdk.ValAddress, sdk.Int) {
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(5)
|
||||
valAccAddr, user := addrs[0], addrs[1]
|
||||
valAddr := sdk.ValAddress(valAccAddr)
|
||||
|
||||
balance := sdk.NewInt(1e9)
|
||||
|
||||
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(balance))
|
||||
suite.CreateAccountWithAddress(user, suite.NewBondCoins(balance))
|
||||
|
||||
suite.CreateNewUnbondedValidator(valAddr, balance)
|
||||
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||
return user, valAddr, balance
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) setupValidatorAndDelegation() (sdk.AccAddress, sdk.ValAddress, sdk.Int) {
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(5)
|
||||
valAccAddr, user := addrs[0], addrs[1]
|
||||
valAddr := sdk.ValAddress(valAccAddr)
|
||||
|
||||
balance := sdk.NewInt(1e9)
|
||||
|
||||
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(balance))
|
||||
suite.CreateAccountWithAddress(user, suite.NewBondCoins(balance))
|
||||
|
||||
suite.CreateNewUnbondedValidator(valAddr, balance)
|
||||
suite.CreateDelegation(valAddr, user, balance)
|
||||
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
|
||||
return user, valAddr, balance
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) setupEarnForDeposits(valAddr sdk.ValAddress) string {
|
||||
suite.CreateVault("bkava", earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, false, nil)
|
||||
derivativeDenom := fmt.Sprintf("bkava-%s", valAddr)
|
||||
suite.SetSavingsSupportedDenoms([]string{derivativeDenom})
|
||||
return derivativeDenom
|
||||
}
|
||||
|
||||
func (suite *msgServerTestSuite) setupDerivatives() (sdk.AccAddress, sdk.ValAddress, sdk.Coin) {
|
||||
user, valAddr, delegation := suite.setupValidatorAndDelegation()
|
||||
suite.setupEarnForDeposits(valAddr)
|
||||
|
||||
msg := types.NewMsgMintDeposit(
|
||||
user,
|
||||
valAddr,
|
||||
suite.NewBondCoin(delegation),
|
||||
)
|
||||
_, err := suite.msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
derivativeDenom := fmt.Sprintf("bkava-%s", valAddr)
|
||||
derivatives, err := suite.EarnKeeper.GetVaultAccountValue(suite.Ctx, derivativeDenom, user)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return user, valAddr, derivatives
|
||||
}
|
137
x/router/module.go
Normal file
137
x/router/module.go
Normal file
@ -0,0 +1,137 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/router/client/cli"
|
||||
"github.com/kava-labs/kava/x/router/keeper"
|
||||
"github.com/kava-labs/kava/x/router/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ module.AppModule = AppModule{}
|
||||
_ module.AppModuleBasic = AppModuleBasic{}
|
||||
)
|
||||
|
||||
// AppModuleBasic app module basics object
|
||||
type AppModuleBasic struct{}
|
||||
|
||||
// Name get module name
|
||||
func (AppModuleBasic) Name() string {
|
||||
return types.ModuleName
|
||||
}
|
||||
|
||||
// RegisterLegacyAminoCodec register module codec
|
||||
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
|
||||
types.RegisterLegacyAminoCodec(cdc)
|
||||
}
|
||||
|
||||
// DefaultGenesis default genesis state
|
||||
func (AppModuleBasic) DefaultGenesis(_ codec.JSONCodec) json.RawMessage {
|
||||
return []byte("{}")
|
||||
}
|
||||
|
||||
// ValidateGenesis module validate genesis
|
||||
func (AppModuleBasic) ValidateGenesis(_ codec.JSONCodec, _ client.TxEncodingConfig, _ json.RawMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterInterfaces implements InterfaceModule.RegisterInterfaces
|
||||
func (a AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) {
|
||||
types.RegisterInterfaces(registry)
|
||||
}
|
||||
|
||||
// RegisterRESTRoutes registers REST routes for the module.
|
||||
func (a AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) {}
|
||||
|
||||
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module.
|
||||
func (a AppModuleBasic) RegisterGRPCGatewayRoutes(_ client.Context, _ *runtime.ServeMux) {
|
||||
}
|
||||
|
||||
// GetTxCmd returns the root tx command for the module.
|
||||
func (AppModuleBasic) GetTxCmd() *cobra.Command {
|
||||
return cli.GetTxCmd()
|
||||
}
|
||||
|
||||
// GetQueryCmd returns no root query command for the module.
|
||||
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
//____________________________________________________________________________
|
||||
|
||||
// AppModule app module type
|
||||
type AppModule struct {
|
||||
AppModuleBasic
|
||||
|
||||
keeper keeper.Keeper
|
||||
}
|
||||
|
||||
// NewAppModule creates a new AppModule object
|
||||
func NewAppModule(keeper keeper.Keeper) AppModule {
|
||||
return AppModule{
|
||||
AppModuleBasic: AppModuleBasic{},
|
||||
keeper: keeper,
|
||||
}
|
||||
}
|
||||
|
||||
// Name module name
|
||||
func (am AppModule) Name() string {
|
||||
return am.AppModuleBasic.Name()
|
||||
}
|
||||
|
||||
// RegisterInvariants register module invariants
|
||||
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
|
||||
|
||||
// Route module message route name
|
||||
func (am AppModule) Route() sdk.Route {
|
||||
return sdk.Route{}
|
||||
}
|
||||
|
||||
// QuerierRoute module querier route name
|
||||
func (AppModule) QuerierRoute() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// LegacyQuerierHandler returns no sdk.Querier.
|
||||
func (am AppModule) LegacyQuerierHandler(_ *codec.LegacyAmino) sdk.Querier {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConsensusVersion implements AppModule/ConsensusVersion.
|
||||
func (AppModule) ConsensusVersion() uint64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// RegisterServices registers module services.
|
||||
func (am AppModule) RegisterServices(cfg module.Configurator) {
|
||||
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
|
||||
}
|
||||
|
||||
// InitGenesis module init-genesis
|
||||
func (am AppModule) InitGenesis(_ sdk.Context, _ codec.JSONCodec, _ json.RawMessage) []abci.ValidatorUpdate {
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
||||
|
||||
// ExportGenesis module export genesis
|
||||
func (am AppModule) ExportGenesis(_ sdk.Context, cdc codec.JSONCodec) json.RawMessage {
|
||||
return am.DefaultGenesis(cdc)
|
||||
}
|
||||
|
||||
// BeginBlock module begin-block
|
||||
func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
|
||||
|
||||
// EndBlock module end-block
|
||||
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
361
x/router/testutil/suite.go
Normal file
361
x/router/testutil/suite.go
Normal file
@ -0,0 +1,361 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
|
||||
"github.com/cosmos/cosmos-sdk/simapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
earnkeeper "github.com/kava-labs/kava/x/earn/keeper"
|
||||
earntypes "github.com/kava-labs/kava/x/earn/types"
|
||||
"github.com/kava-labs/kava/x/router/keeper"
|
||||
savingstypes "github.com/kava-labs/kava/x/savings/types"
|
||||
)
|
||||
|
||||
// Test suite used for all keeper tests
|
||||
type Suite struct {
|
||||
suite.Suite
|
||||
App app.TestApp
|
||||
Ctx sdk.Context
|
||||
Keeper keeper.Keeper
|
||||
BankKeeper bankkeeper.Keeper
|
||||
StakingKeeper stakingkeeper.Keeper
|
||||
EarnKeeper earnkeeper.Keeper
|
||||
}
|
||||
|
||||
// The default state used by each test
|
||||
func (suite *Suite) SetupTest() {
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
|
||||
|
||||
tApp.InitializeFromGenesisStates()
|
||||
|
||||
suite.App = tApp
|
||||
suite.Ctx = ctx
|
||||
suite.Keeper = tApp.GetRouterKeeper()
|
||||
suite.StakingKeeper = tApp.GetStakingKeeper()
|
||||
suite.BankKeeper = tApp.GetBankKeeper()
|
||||
suite.EarnKeeper = tApp.GetEarnKeeper()
|
||||
}
|
||||
|
||||
// CreateAccount creates a new account from the provided balance and address
|
||||
func (suite *Suite) CreateAccountWithAddress(addr sdk.AccAddress, initialBalance sdk.Coins) authtypes.AccountI {
|
||||
ak := suite.App.GetAccountKeeper()
|
||||
|
||||
acc := ak.NewAccountWithAddress(suite.Ctx, addr)
|
||||
ak.SetAccount(suite.Ctx, acc)
|
||||
|
||||
err := simapp.FundAccount(suite.BankKeeper, suite.Ctx, acc.GetAddress(), initialBalance)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
// CreateVestingAccount creates a new vesting account. `vestingBalance` should be a fraction of `initialBalance`.
|
||||
func (suite *Suite) CreateVestingAccountWithAddress(addr sdk.AccAddress, initialBalance sdk.Coins, vestingBalance sdk.Coins) authtypes.AccountI {
|
||||
if vestingBalance.IsAnyGT(initialBalance) {
|
||||
panic("vesting balance must be less than initial balance")
|
||||
}
|
||||
acc := suite.CreateAccountWithAddress(addr, initialBalance)
|
||||
bacc := acc.(*authtypes.BaseAccount)
|
||||
|
||||
periods := vestingtypes.Periods{
|
||||
vestingtypes.Period{
|
||||
Length: 31556952,
|
||||
Amount: vestingBalance,
|
||||
},
|
||||
}
|
||||
vacc := vestingtypes.NewPeriodicVestingAccount(bacc, vestingBalance, suite.Ctx.BlockTime().Unix(), periods)
|
||||
suite.App.GetAccountKeeper().SetAccount(suite.Ctx, vacc)
|
||||
return vacc
|
||||
}
|
||||
|
||||
// AddCoinsToModule adds coins to the a module account, creating it if it doesn't exist.
|
||||
func (suite *Suite) AddCoinsToModule(module string, amount sdk.Coins) {
|
||||
err := simapp.FundModuleAccount(suite.BankKeeper, suite.Ctx, module, amount)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
// AccountBalanceEqual checks if an account has the specified coins.
|
||||
func (suite *Suite) AccountBalanceEqual(addr sdk.AccAddress, coins sdk.Coins) {
|
||||
balance := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
|
||||
suite.Equalf(coins, balance, "expected account balance to equal coins %s, but got %s", coins, balance)
|
||||
}
|
||||
|
||||
// AccountBalanceOfEqual checks if an account has the specified amount of one denom.
|
||||
func (suite *Suite) AccountBalanceOfEqual(addr sdk.AccAddress, denom string, amount sdk.Int) {
|
||||
balance := suite.BankKeeper.GetBalance(suite.Ctx, addr, denom).Amount
|
||||
suite.Equalf(amount, balance, "expected account balance to have %[1]s%[2]s, but got %[3]s%[2]s", amount, denom, balance)
|
||||
}
|
||||
|
||||
// AccountSpendableBalanceEqual checks if an account has the specified coins unlocked.
|
||||
func (suite *Suite) AccountSpendableBalanceEqual(addr sdk.AccAddress, amount sdk.Coins) {
|
||||
balance := suite.BankKeeper.SpendableCoins(suite.Ctx, addr)
|
||||
suite.Equalf(amount, balance, "expected account spendable balance to equal coins %s, but got %s", amount, balance)
|
||||
}
|
||||
|
||||
func (suite *Suite) QueryBank_SpendableBalance(user sdk.AccAddress) sdk.Coins {
|
||||
res, err := suite.BankKeeper.SpendableBalances(
|
||||
sdk.WrapSDKContext(suite.Ctx),
|
||||
&banktypes.QuerySpendableBalancesRequest{
|
||||
Address: user.String(),
|
||||
},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
return *&res.Balances
|
||||
}
|
||||
|
||||
func (suite *Suite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error {
|
||||
msg, err := stakingtypes.NewMsgCreateValidator(
|
||||
address,
|
||||
ed25519.GenPrivKey().PubKey(),
|
||||
selfDelegation,
|
||||
stakingtypes.Description{},
|
||||
stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
|
||||
sdk.NewInt(1e6),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
|
||||
_, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewBondCoin creates a Coin with the current staking denom.
|
||||
func (suite *Suite) NewBondCoin(amount sdk.Int) sdk.Coin {
|
||||
stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
|
||||
return sdk.NewCoin(stakingDenom, amount)
|
||||
}
|
||||
|
||||
// NewBondCoins creates Coins with the current staking denom.
|
||||
func (suite *Suite) NewBondCoins(amount sdk.Int) sdk.Coins {
|
||||
return sdk.NewCoins(suite.NewBondCoin(amount))
|
||||
}
|
||||
|
||||
// CreateNewUnbondedValidator creates a new validator in the staking module.
|
||||
// New validators are unbonded until the end blocker is run.
|
||||
func (suite *Suite) CreateNewUnbondedValidator(addr sdk.ValAddress, selfDelegation sdk.Int) stakingtypes.Validator {
|
||||
// Create a validator
|
||||
err := suite.deliverMsgCreateValidator(suite.Ctx, addr, suite.NewBondCoin(selfDelegation))
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// New validators are created in an unbonded state. Note if the end blocker is run later this validator could become bonded.
|
||||
|
||||
validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, addr)
|
||||
suite.Require().True(found)
|
||||
return validator
|
||||
}
|
||||
|
||||
// SlashValidator burns tokens staked in a validator. new_tokens = old_tokens * (1-slashFraction)
|
||||
func (suite *Suite) SlashValidator(addr sdk.ValAddress, slashFraction sdk.Dec) {
|
||||
validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, addr)
|
||||
suite.Require().True(found)
|
||||
consAddr, err := validator.GetConsAddr()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Assume infraction was at current height. Note unbonding delegations and redelegations are only slashed if created after
|
||||
// the infraction height so none will be slashed.
|
||||
infractionHeight := suite.Ctx.BlockHeight()
|
||||
|
||||
power := suite.StakingKeeper.TokensToConsensusPower(suite.Ctx, validator.GetTokens())
|
||||
|
||||
suite.StakingKeeper.Slash(suite.Ctx, consAddr, infractionHeight, power, slashFraction)
|
||||
}
|
||||
|
||||
// CreateDelegation delegates tokens to a validator.
|
||||
func (suite *Suite) CreateDelegation(valAddr sdk.ValAddress, delegator sdk.AccAddress, amount sdk.Int) sdk.Dec {
|
||||
stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
|
||||
msg := stakingtypes.NewMsgDelegate(
|
||||
delegator,
|
||||
valAddr,
|
||||
sdk.NewCoin(stakingDenom, amount),
|
||||
)
|
||||
|
||||
msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
|
||||
_, err := msgServer.Delegate(sdk.WrapSDKContext(suite.Ctx), msg)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
|
||||
suite.Require().True(found)
|
||||
return del.Shares
|
||||
}
|
||||
|
||||
// DelegationSharesEqual checks if a delegation has the specified shares.
|
||||
// It expects delegations with zero shares to not be stored in state.
|
||||
func (suite *Suite) DelegationSharesEqual(valAddr sdk.ValAddress, delegator sdk.AccAddress, shares sdk.Dec) bool {
|
||||
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
|
||||
|
||||
if shares.IsZero() {
|
||||
return suite.Falsef(found, "expected delegator to not be found, got %s shares", del.Shares)
|
||||
} else {
|
||||
res := suite.True(found, "expected delegator to be found")
|
||||
return res && suite.Truef(shares.Equal(del.Shares), "expected %s delegator shares but got %s", shares, del.Shares)
|
||||
}
|
||||
}
|
||||
|
||||
// DelegationBalanceLessThan checks if a delegation's staked token balance is less the specified amount.
|
||||
// It treats not found delegations as having zero shares.
|
||||
func (suite *Suite) DelegationBalanceLessThan(valAddr sdk.ValAddress, delegator sdk.AccAddress, max sdk.Int) bool {
|
||||
shares := sdk.ZeroDec()
|
||||
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
|
||||
if found {
|
||||
shares = del.Shares
|
||||
}
|
||||
|
||||
val, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
|
||||
suite.Require().Truef(found, "expected validator to be found")
|
||||
|
||||
tokens := val.TokensFromShares(shares).TruncateInt()
|
||||
|
||||
return suite.Truef(tokens.LT(max), "expected delegation balance to be less than %s, got %s", max, tokens)
|
||||
}
|
||||
|
||||
// DelegationBalanceInDeltaBelow checks if a delegation's staked token balance is between `expected` and `expected - delta` inclusive.
|
||||
// It treats not found delegations as having zero shares.
|
||||
func (suite *Suite) DelegationBalanceInDeltaBelow(valAddr sdk.ValAddress, delegator sdk.AccAddress, expected, delta sdk.Int) bool {
|
||||
shares := sdk.ZeroDec()
|
||||
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
|
||||
if found {
|
||||
shares = del.Shares
|
||||
}
|
||||
|
||||
val, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
|
||||
suite.Require().Truef(found, "expected validator to be found")
|
||||
|
||||
tokens := val.TokensFromShares(shares).TruncateInt()
|
||||
|
||||
lte := suite.Truef(tokens.LTE(expected), "expected delegation balance to be less than or equal to %s, got %s", expected, tokens)
|
||||
gte := suite.Truef(tokens.GTE(expected.Sub(delta)), "expected delegation balance to be greater than or equal to %s, got %s", expected.Sub(delta), tokens)
|
||||
return lte && gte
|
||||
}
|
||||
|
||||
// UnbondingDelegationInDeltaBelow checks if the total balance in an unbonding delegation is between `expected` and `expected - delta` inclusive.
|
||||
func (suite *Suite) UnbondingDelegationInDeltaBelow(valAddr sdk.ValAddress, delegator sdk.AccAddress, expected, delta sdk.Int) bool {
|
||||
tokens := sdk.ZeroInt()
|
||||
ubd, found := suite.StakingKeeper.GetUnbondingDelegation(suite.Ctx, delegator, valAddr)
|
||||
if found {
|
||||
for _, entry := range ubd.Entries {
|
||||
tokens = tokens.Add(entry.Balance)
|
||||
}
|
||||
}
|
||||
|
||||
lte := suite.Truef(tokens.LTE(expected), "expected unbonding delegation balance to be less than or equal to %s, got %s", expected, tokens)
|
||||
gte := suite.Truef(tokens.GTE(expected.Sub(delta)), "expected unbonding delegation balance to be greater than or equal to %s, got %s", expected.Sub(delta), tokens)
|
||||
return lte && gte
|
||||
}
|
||||
|
||||
func (suite *Suite) QueryStaking_Delegation(valAddr sdk.ValAddress, delegator sdk.AccAddress) stakingtypes.DelegationResponse {
|
||||
stakingQuery := stakingkeeper.Querier{Keeper: suite.StakingKeeper}
|
||||
res, err := stakingQuery.Delegation(
|
||||
sdk.WrapSDKContext(suite.Ctx),
|
||||
&stakingtypes.QueryDelegationRequest{
|
||||
DelegatorAddr: delegator.String(),
|
||||
ValidatorAddr: valAddr.String(),
|
||||
},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
return *res.DelegationResponse
|
||||
}
|
||||
|
||||
// EventsContains asserts that the expected event is in the provided events
|
||||
func (suite *Suite) EventsContains(events sdk.Events, expectedEvent sdk.Event) {
|
||||
foundMatch := false
|
||||
for _, event := range events {
|
||||
if event.Type == expectedEvent.Type {
|
||||
if reflect.DeepEqual(attrsToMap(expectedEvent.Attributes), attrsToMap(event.Attributes)) {
|
||||
foundMatch = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite.True(foundMatch, fmt.Sprintf("event of type %s not found or did not match", expectedEvent.Type))
|
||||
}
|
||||
|
||||
func attrsToMap(attrs []abci.EventAttribute) []sdk.Attribute {
|
||||
out := []sdk.Attribute{}
|
||||
|
||||
for _, attr := range attrs {
|
||||
out = append(out, sdk.NewAttribute(string(attr.Key), string(attr.Value)))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// CreateVault adds a new earn vault to the earn keeper parameters
|
||||
func (suite *Suite) CreateVault(
|
||||
vaultDenom string,
|
||||
vaultStrategies earntypes.StrategyTypes,
|
||||
isPrivateVault bool,
|
||||
allowedDepositors []sdk.AccAddress,
|
||||
) {
|
||||
vault := earntypes.NewAllowedVault(vaultDenom, vaultStrategies, isPrivateVault, allowedDepositors)
|
||||
|
||||
allowedVaults := suite.EarnKeeper.GetAllowedVaults(suite.Ctx)
|
||||
allowedVaults = append(allowedVaults, vault)
|
||||
|
||||
params := earntypes.NewParams(allowedVaults)
|
||||
|
||||
suite.EarnKeeper.SetParams(
|
||||
suite.Ctx,
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
// SetSavingsSupportedDenoms overwrites the list of supported denoms in the savings module params.
|
||||
func (suite *Suite) SetSavingsSupportedDenoms(denoms []string) {
|
||||
sk := suite.App.GetSavingsKeeper()
|
||||
sk.SetParams(suite.Ctx, savingstypes.NewParams(denoms))
|
||||
}
|
||||
|
||||
// VaultAccountValueEqual asserts that the vault account value matches the provided coin amount.
|
||||
func (suite *Suite) VaultAccountValueEqual(acc sdk.AccAddress, coin sdk.Coin) {
|
||||
|
||||
accVaultBal, err := suite.EarnKeeper.GetVaultAccountValue(suite.Ctx, coin.Denom, acc)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Require().Truef(
|
||||
coin.Equal(accVaultBal),
|
||||
"expected account vault balance to equal %s, but got %s",
|
||||
coin, accVaultBal,
|
||||
)
|
||||
}
|
||||
|
||||
// VaultAccountSharesEqual asserts that the vault account shares match the provided values.
|
||||
func (suite *Suite) VaultAccountSharesEqual(acc sdk.AccAddress, shares earntypes.VaultShares) { // TODO
|
||||
|
||||
accVaultShares, found := suite.EarnKeeper.GetVaultAccountShares(suite.Ctx, acc)
|
||||
if !found {
|
||||
suite.Empty(shares)
|
||||
} else {
|
||||
suite.Equal(shares, accVaultShares)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Suite) QueryEarn_VaultValue(depositor sdk.AccAddress, vaultDenom string) earntypes.DepositResponse {
|
||||
earnQuery := earnkeeper.NewQueryServerImpl(suite.EarnKeeper)
|
||||
res, err := earnQuery.Deposits(
|
||||
sdk.WrapSDKContext(suite.Ctx),
|
||||
&earntypes.QueryDepositsRequest{
|
||||
Depositor: depositor.String(),
|
||||
Denom: vaultDenom,
|
||||
},
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
suite.Require().Equalf(1, len(res.Deposits), "while earn supports one vault per denom, deposits response should be length 1")
|
||||
return res.Deposits[0]
|
||||
}
|
42
x/router/types/codec.go
Normal file
42
x/router/types/codec.go
Normal file
@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/codec/types"
|
||||
|
||||
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/msgservice"
|
||||
)
|
||||
|
||||
// RegisterLegacyAminoCodec registers all the necessary types and interfaces for the module.
|
||||
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
|
||||
cdc.RegisterConcrete(&MsgMintDeposit{}, "router/MsgMintDeposit", nil)
|
||||
cdc.RegisterConcrete(&MsgDelegateMintDeposit{}, "router/MsgDelegateMintDeposit", nil)
|
||||
cdc.RegisterConcrete(&MsgWithdrawBurn{}, "router/MsgWithdrawBurn", nil)
|
||||
cdc.RegisterConcrete(&MsgWithdrawBurnUndelegate{}, "router/MsgWithdrawBurnUndelegate", nil)
|
||||
}
|
||||
|
||||
// RegisterInterfaces registers proto messages under their interfaces for unmarshalling,
|
||||
// in addition to registering the msg service for handling tx msgs
|
||||
func RegisterInterfaces(registry types.InterfaceRegistry) {
|
||||
registry.RegisterImplementations((*sdk.Msg)(nil),
|
||||
&MsgMintDeposit{},
|
||||
&MsgDelegateMintDeposit{},
|
||||
&MsgWithdrawBurn{},
|
||||
&MsgWithdrawBurnUndelegate{},
|
||||
)
|
||||
|
||||
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
|
||||
}
|
||||
|
||||
var (
|
||||
amino = codec.NewLegacyAmino()
|
||||
// ModuleCdc represents the legacy amino codec for the module
|
||||
ModuleCdc = codec.NewAminoCodec(amino)
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterLegacyAminoCodec(amino)
|
||||
cryptocodec.RegisterCrypto(amino)
|
||||
}
|
13
x/router/types/common_test.go
Normal file
13
x/router/types/common_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
app.SetSDKConfig()
|
||||
os.Exit(m.Run())
|
||||
}
|
34
x/router/types/expected_keepers.go
Normal file
34
x/router/types/expected_keepers.go
Normal file
@ -0,0 +1,34 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
|
||||
earntypes "github.com/kava-labs/kava/x/earn/types"
|
||||
)
|
||||
|
||||
type StakingKeeper interface {
|
||||
BondDenom(ctx sdk.Context) (res string)
|
||||
GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool)
|
||||
|
||||
Delegate(
|
||||
ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Int, tokenSrc stakingtypes.BondStatus,
|
||||
validator stakingtypes.Validator, subtractAccount bool,
|
||||
) (newShares sdk.Dec, err error)
|
||||
Undelegate(
|
||||
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec,
|
||||
) (time.Time, error)
|
||||
}
|
||||
|
||||
type LiquidKeeper interface {
|
||||
DerivativeFromTokens(ctx sdk.Context, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Coin, error)
|
||||
MintDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Coin, error)
|
||||
BurnDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Dec, error)
|
||||
}
|
||||
|
||||
type EarnKeeper interface {
|
||||
Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositStrategy earntypes.StrategyType) error
|
||||
Withdraw(ctx sdk.Context, from sdk.AccAddress, wantAmount sdk.Coin, withdrawStrategy earntypes.StrategyType) error
|
||||
}
|
9
x/router/types/keys.go
Normal file
9
x/router/types/keys.go
Normal file
@ -0,0 +1,9 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
// ModuleName name that will be used throughout the module
|
||||
ModuleName = "router"
|
||||
|
||||
// RouterKey top level router key
|
||||
RouterKey = ModuleName
|
||||
)
|
201
x/router/types/msg.go
Normal file
201
x/router/types/msg.go
Normal file
@ -0,0 +1,201 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeMsgMintDeposit defines the type for MsgMintDeposit
|
||||
TypeMsgMintDeposit = "mint_deposit"
|
||||
// TypeMsgDelegateMintDeposit defines the type for MsgDelegateMintDeposit
|
||||
TypeMsgDelegateMintDeposit = "delegate_mint_deposit"
|
||||
// TypeMsgWithdrawBurn defines the type for MsgWithdrawBurn
|
||||
TypeMsgWithdrawBurn = "withdraw_burn"
|
||||
// TypeMsgWithdrawBurnUndelegate defines the type for MsgWithdrawBurnUndelegate
|
||||
TypeMsgWithdrawBurnUndelegate = "withdraw_burn_undelegate"
|
||||
)
|
||||
|
||||
var (
|
||||
_ sdk.Msg = &MsgMintDeposit{}
|
||||
_ legacytx.LegacyMsg = &MsgMintDeposit{}
|
||||
_ sdk.Msg = &MsgDelegateMintDeposit{}
|
||||
_ legacytx.LegacyMsg = &MsgDelegateMintDeposit{}
|
||||
_ sdk.Msg = &MsgWithdrawBurn{}
|
||||
_ legacytx.LegacyMsg = &MsgWithdrawBurn{}
|
||||
_ sdk.Msg = &MsgWithdrawBurnUndelegate{}
|
||||
_ legacytx.LegacyMsg = &MsgWithdrawBurnUndelegate{}
|
||||
)
|
||||
|
||||
// NewMsgMintDeposit returns a new MsgMintDeposit.
|
||||
func NewMsgMintDeposit(depositor sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgMintDeposit {
|
||||
return &MsgMintDeposit{
|
||||
Depositor: depositor.String(),
|
||||
Validator: validator.String(),
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgMintDeposit) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgMintDeposit) Type() string { return TypeMsgMintDeposit }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||
func (msg MsgMintDeposit) ValidateBasic() error {
|
||||
if _, err := sdk.AccAddressFromBech32(msg.Depositor); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid depositor address: %s", err)
|
||||
}
|
||||
|
||||
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
|
||||
}
|
||||
|
||||
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgMintDeposit) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(&msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgMintDeposit) GetSigners() []sdk.AccAddress {
|
||||
depositor, _ := sdk.AccAddressFromBech32(msg.Depositor)
|
||||
return []sdk.AccAddress{depositor}
|
||||
}
|
||||
|
||||
// NewMsgDelegateMintDeposit returns a new MsgDelegateMintDeposit.
|
||||
func NewMsgDelegateMintDeposit(depositor sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgDelegateMintDeposit {
|
||||
return &MsgDelegateMintDeposit{
|
||||
Depositor: depositor.String(),
|
||||
Validator: validator.String(),
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgDelegateMintDeposit) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgDelegateMintDeposit) Type() string { return TypeMsgDelegateMintDeposit }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||
func (msg MsgDelegateMintDeposit) ValidateBasic() error {
|
||||
if _, err := sdk.AccAddressFromBech32(msg.Depositor); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid depositor address: %s", err)
|
||||
}
|
||||
|
||||
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
|
||||
}
|
||||
|
||||
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgDelegateMintDeposit) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(&msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgDelegateMintDeposit) GetSigners() []sdk.AccAddress {
|
||||
depositor, _ := sdk.AccAddressFromBech32(msg.Depositor)
|
||||
return []sdk.AccAddress{depositor}
|
||||
}
|
||||
|
||||
// NewMsgWithdrawBurn returns a new MsgWithdrawBurn.
|
||||
func NewMsgWithdrawBurn(from sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgWithdrawBurn {
|
||||
return &MsgWithdrawBurn{
|
||||
From: from.String(),
|
||||
Validator: validator.String(),
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgWithdrawBurn) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgWithdrawBurn) Type() string { return TypeMsgWithdrawBurn }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||
func (msg MsgWithdrawBurn) ValidateBasic() error {
|
||||
if _, err := sdk.AccAddressFromBech32(msg.From); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid from address: %s", err)
|
||||
}
|
||||
|
||||
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
|
||||
}
|
||||
|
||||
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgWithdrawBurn) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(&msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgWithdrawBurn) GetSigners() []sdk.AccAddress {
|
||||
from, _ := sdk.AccAddressFromBech32(msg.From)
|
||||
return []sdk.AccAddress{from}
|
||||
}
|
||||
|
||||
// NewMsgWithdrawBurnUndelegate returns a new MsgWithdrawBurnUndelegate.
|
||||
func NewMsgWithdrawBurnUndelegate(from sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgWithdrawBurnUndelegate {
|
||||
return &MsgWithdrawBurnUndelegate{
|
||||
From: from.String(),
|
||||
Validator: validator.String(),
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
// Route return the message type used for routing the message.
|
||||
func (msg MsgWithdrawBurnUndelegate) Route() string { return RouterKey }
|
||||
|
||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||
func (msg MsgWithdrawBurnUndelegate) Type() string { return TypeMsgWithdrawBurnUndelegate }
|
||||
|
||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||
func (msg MsgWithdrawBurnUndelegate) ValidateBasic() error {
|
||||
if _, err := sdk.AccAddressFromBech32(msg.From); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid from address: %s", err)
|
||||
}
|
||||
|
||||
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
|
||||
}
|
||||
|
||||
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgWithdrawBurnUndelegate) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(&msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// GetSigners returns the addresses of signers that must sign.
|
||||
func (msg MsgWithdrawBurnUndelegate) GetSigners() []sdk.AccAddress {
|
||||
from, _ := sdk.AccAddressFromBech32(msg.From)
|
||||
return []sdk.AccAddress{from}
|
||||
}
|
207
x/router/types/msg_test.go
Normal file
207
x/router/types/msg_test.go
Normal file
@ -0,0 +1,207 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/kava-labs/kava/x/router/types"
|
||||
)
|
||||
|
||||
func TestMsgMintDeposit_Signing(t *testing.T) {
|
||||
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
|
||||
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
|
||||
|
||||
msg := types.NewMsgMintDeposit(
|
||||
address,
|
||||
validatorAddress,
|
||||
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
|
||||
)
|
||||
|
||||
// checking for the "type" field ensures the msg is registered on the amino codec
|
||||
signBytes := []byte(
|
||||
`{"type":"router/MsgMintDeposit","value":{"amount":{"amount":"1000000000","denom":"ukava"},"depositor":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
|
||||
)
|
||||
|
||||
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
|
||||
assert.Equal(t, signBytes, msg.GetSignBytes())
|
||||
}
|
||||
|
||||
func TestMsgDelegateMintDeposit_Signing(t *testing.T) {
|
||||
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
|
||||
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
|
||||
|
||||
msg := types.NewMsgDelegateMintDeposit(
|
||||
address,
|
||||
validatorAddress,
|
||||
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
|
||||
)
|
||||
|
||||
// checking for the "type" field ensures the msg is registered on the amino codec
|
||||
signBytes := []byte(
|
||||
`{"type":"router/MsgDelegateMintDeposit","value":{"amount":{"amount":"1000000000","denom":"ukava"},"depositor":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
|
||||
)
|
||||
|
||||
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
|
||||
assert.Equal(t, signBytes, msg.GetSignBytes())
|
||||
}
|
||||
|
||||
func TestMsgWithdrawBurn_Signing(t *testing.T) {
|
||||
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
|
||||
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
|
||||
|
||||
msg := types.NewMsgWithdrawBurn(
|
||||
address,
|
||||
validatorAddress,
|
||||
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
|
||||
)
|
||||
|
||||
// checking for the "type" field ensures the msg is registered on the amino codec
|
||||
signBytes := []byte(
|
||||
`{"type":"router/MsgWithdrawBurn","value":{"amount":{"amount":"1000000000","denom":"ukava"},"from":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
|
||||
)
|
||||
|
||||
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
|
||||
assert.Equal(t, signBytes, msg.GetSignBytes())
|
||||
}
|
||||
|
||||
func TestMsgWithdrawBurnUndelegate_Signing(t *testing.T) {
|
||||
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
|
||||
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
|
||||
|
||||
msg := types.NewMsgWithdrawBurnUndelegate(
|
||||
address,
|
||||
validatorAddress,
|
||||
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
|
||||
)
|
||||
|
||||
// checking for the "type" field ensures the msg is registered on the amino codec
|
||||
signBytes := []byte(
|
||||
`{"type":"router/MsgWithdrawBurnUndelegate","value":{"amount":{"amount":"1000000000","denom":"ukava"},"from":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
|
||||
)
|
||||
|
||||
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
|
||||
assert.Equal(t, signBytes, msg.GetSignBytes())
|
||||
}
|
||||
|
||||
func TestMsg_Validate(t *testing.T) {
|
||||
validAddress := "kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d"
|
||||
validValidatorAddress := "kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"
|
||||
validCoin := sdk.NewInt64Coin("ukava", 1e9)
|
||||
|
||||
type msgArgs struct {
|
||||
depositor string
|
||||
validator string
|
||||
amount sdk.Coin
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
msgArgs msgArgs
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "normal multiplier is valid",
|
||||
msgArgs: msgArgs{
|
||||
depositor: validAddress,
|
||||
validator: validValidatorAddress,
|
||||
amount: validCoin,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid depositor",
|
||||
msgArgs: msgArgs{
|
||||
depositor: "invalid",
|
||||
validator: validValidatorAddress,
|
||||
amount: validCoin,
|
||||
},
|
||||
expectedErr: sdkerrors.ErrInvalidAddress,
|
||||
},
|
||||
{
|
||||
name: "empty depositor",
|
||||
msgArgs: msgArgs{
|
||||
depositor: "",
|
||||
validator: validValidatorAddress,
|
||||
amount: validCoin,
|
||||
},
|
||||
expectedErr: sdkerrors.ErrInvalidAddress,
|
||||
},
|
||||
{
|
||||
name: "invalid validator",
|
||||
msgArgs: msgArgs{
|
||||
depositor: validAddress,
|
||||
validator: "invalid",
|
||||
amount: validCoin,
|
||||
},
|
||||
expectedErr: sdkerrors.ErrInvalidAddress,
|
||||
},
|
||||
{
|
||||
name: "nil coin",
|
||||
msgArgs: msgArgs{
|
||||
depositor: validAddress,
|
||||
validator: validValidatorAddress,
|
||||
amount: sdk.Coin{},
|
||||
},
|
||||
expectedErr: sdkerrors.ErrInvalidCoins,
|
||||
},
|
||||
{
|
||||
name: "zero coin",
|
||||
msgArgs: msgArgs{
|
||||
depositor: validAddress,
|
||||
validator: validValidatorAddress,
|
||||
amount: sdk.NewCoin("ukava", sdk.ZeroInt()),
|
||||
},
|
||||
expectedErr: sdkerrors.ErrInvalidCoins,
|
||||
},
|
||||
{
|
||||
name: "negative coin",
|
||||
msgArgs: msgArgs{
|
||||
depositor: validAddress,
|
||||
validator: validValidatorAddress,
|
||||
amount: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
|
||||
},
|
||||
expectedErr: sdkerrors.ErrInvalidCoins,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
msgMintDeposit := types.MsgMintDeposit{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
|
||||
msgDelegateMintDeposit := types.MsgDelegateMintDeposit{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
|
||||
|
||||
msgWithdrawBurn := types.MsgWithdrawBurn{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
|
||||
msgWithdrawBurnUndelegate := types.MsgWithdrawBurnUndelegate{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
|
||||
|
||||
msgs := []sdk.Msg{&msgMintDeposit, &msgDelegateMintDeposit, &msgWithdrawBurn, &msgWithdrawBurnUndelegate}
|
||||
for _, msg := range msgs {
|
||||
t.Run(fmt.Sprintf("%T", msg), func(t *testing.T) {
|
||||
err := msg.ValidateBasic()
|
||||
if tc.expectedErr == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorIs(t, err, tc.expectedErr, "expected error '%s' not found in actual '%s'", tc.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustAccAddressFromBech32(address string) sdk.AccAddress {
|
||||
addr, err := sdk.AccAddressFromBech32(address)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func mustValAddressFromBech32(address string) sdk.ValAddress {
|
||||
addr, err := sdk.ValAddressFromBech32(address)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return addr
|
||||
}
|
1882
x/router/types/tx.pb.go
Normal file
1882
x/router/types/tx.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user