Skip to content

Commit

Permalink
feat!: add MsgTerminatePrivatePlan (#154)
Browse files Browse the repository at this point in the history
* feat: add MsgTerminatePrivatePlan

* test: add tests and fix code a bit

* fix: revert changes in client docs

* docs: remove broken link and deprecated ignite guide

---------

Co-authored-by: King <[email protected]>
  • Loading branch information
crypin and kingcre authored May 16, 2023
1 parent 2232323 commit 89de396
Show file tree
Hide file tree
Showing 18 changed files with 675 additions and 78 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ The documentation is available in [docs](docs) directory. If you are a developer
* [Crescent Official Docs](https://docs.crescent.network/)
* [Swagger API Docs](https://app.swaggerhub.com/apis-docs/crescent/crescent/2.0.0)

<!-- markdown-link-check-disable -->

## Community

* [Official Website](https://crescent.network/)
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/claim.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Description: A high-level overview of how the command-line interfaces (CLI) work

## Synopsis

This document provides a high-level overview of how the command line (CLI) interface works for the `claim` module. To set up a local testing environment, it requires 0.24.1 or lower versions of [Ignite CLI](https://docs.ignite.com/). If you don't have Ignite CLI set up in your local machine, see [this guide](https://docs.ignite.com/guide/install.html) to install it. Run this command under the project root directory `$ ignite chain serve -c config-test.yml`.
This document provides a high-level overview of how the command line (CLI) interface works for the `claim` module.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout the document.

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/farming.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Description: A high-level overview of how the command-line interfaces (CLI) work

## Synopsis

This document provides a high-level overview of how the command line (CLI) interface works for the `farming` module. To set up a local testing environment, it requires 0.24.1 or lower versions of [Ignite CLI](https://docs.ignite.com/). If you don't have Ignite CLI set up in your local machine, see [this guide](https://docs.ignite.com/guide/install.html) to install it. Run this command under the project root directory `$ ignite chain serve -c config-test.yml`.
This document provides a high-level overview of how the command line (CLI) interface works for the `farming` module.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout the document.

Expand Down
3 changes: 0 additions & 3 deletions docs/cli/liquidfarming.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ Description: A high-level overview of how the command-line interfaces (CLI) work
## Synopsis

This document provides a high-level overview of how the command line (CLI) interface works for the `liquidfarming` module.
To set up a local testing environment, it requires 0.24.1 or lower versions of [Ignite CLI](https://docs.ignite.com/).
If you don't have Ignite CLI set up in your local machine, see [this guide](https://docs.ignite.com/guide/install.html) to install it.
Run this command under the project root directory `$ ignite chain serve -c config-test.yml`.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout the document.

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/liquidity.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Description: A high-level overview of how the command-line interfaces (CLI) work

## Synopsis

This document provides a high-level overview of how the command line (CLI) interface works for the `liquidity` module. To set up a local testing environment, it requires 0.24.1 or lower versions of [Ignite CLI](https://docs.ignite.com/). If you don't have Ignite CLI set up in your local machine, see [this guide](https://docs.ignite.com/guide/install.html) to install it. Run this command under the project root directory `$ ignite chain serve -c config-test.yml`.
This document provides a high-level overview of how the command line (CLI) interface works for the `liquidity` module.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout the document.

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/liquidstaking.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Description: A high-level overview of how the command-line interfaces (CLI) work

## Synopsis

This document provides a high-level overview of how the command line (CLI) interface works for the `liquidstaking` module. To set up a local testing environment, it requires 0.24.1 or lower versions of [Ignite CLI](https://docs.ignite.com/). If you don't have Ignite CLI set up in your local machine, see [this guide](https://docs.ignite.com/guide/install.html) to install it. Run this command under the project root directory `$ ignite chain serve -c config-test.yml`.
This document provides a high-level overview of how the command line (CLI) interface works for the `liquidstaking` module.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout the document.

Expand Down
6 changes: 1 addition & 5 deletions docs/cli/lpfarm.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ Description: A high-level overview of how the command-line interfaces (CLI) work
## Synopsis

This document provides a high-level overview of how the command line (CLI)
interface works for the `lpfarm` module. To set up a local testing environment, it requires the latest
[Ignite CLI](https://docs.ignite.com/). If you don't have Ignite CLI set up in your local machine,
see [this guide](https://docs.ignite.com/guide/install.html) to install it. Run this command under the project root
directory
`$ ignite chain serve -v -c config-test.yml`.
interface works for the `lpfarm` module.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout
the document.
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/marketmaker.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Description: A high-level overview of how the command-line interfaces (CLI) work

## Synopsis

This document provides a high-level overview of how the command line (CLI) interface works for the `marketmaker` module. To set up a local testing environment, it requires 0.24.1 or lower versions of [Ignite CLI](https://docs.ignite.com/). If you don't have Ignite CLI set up in your local machine, see [this guide](https://docs.ignite.com/guide/install.html) to install it. Run this command under the project root directory `$ ignite chain serve -v -c config-test.yml`.
This document provides a high-level overview of how the command line (CLI) interface works for the `marketmaker` module.

Note that [jq](https://stedolan.github.io/jq/) is recommended to be installed as it is used to process JSON throughout the document.

Expand Down
8 changes: 8 additions & 0 deletions proto/crescent/lpfarm/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ option (gogoproto.goproto_getters_all) = false;

service Msg {
rpc CreatePrivatePlan(MsgCreatePrivatePlan) returns (MsgCreatePrivatePlanResponse);
rpc TerminatePrivatePlan(MsgTerminatePrivatePlan) returns (MsgTerminatePrivatePlanResponse);
rpc Farm(MsgFarm) returns (MsgFarmResponse);
rpc Unfarm(MsgUnfarm) returns (MsgUnfarmResponse);
rpc Harvest(MsgHarvest) returns (MsgHarvestResponse);
Expand All @@ -30,6 +31,13 @@ message MsgCreatePrivatePlanResponse {
string farming_pool_address = 2;
}

message MsgTerminatePrivatePlan {
string creator = 1;
uint64 plan_id = 2;
}

message MsgTerminatePrivatePlanResponse {}

message MsgFarm {
string farmer = 1;
cosmos.base.v1beta1.Coin coin = 2 [(gogoproto.nullable) = false];
Expand Down
37 changes: 37 additions & 0 deletions x/lpfarm/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func GetTxCmd() *cobra.Command {

cmd.AddCommand(
NewCreatePrivatePlanCmd(),
NewTerminatePrivatePlanCmd(),
NewFarmCmd(),
NewUnfarmCmd(),
NewHarvestCmd(),
Expand Down Expand Up @@ -117,6 +118,42 @@ $ %s tx %s create-private-plan "New Farming Plan" 2022-01-01T00:00:00Z 2023-01-0
return cmd
}

func NewTerminatePrivatePlanCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "terminate-private-plan [plan-id]",
Args: cobra.ExactArgs(1),
Short: "Terminate a private farming plan",
Long: strings.TrimSpace(
fmt.Sprintf(`Terminate a private farming plan.
The plan's termination address must be same with the message sender(original plan creator).
Example:
$ %s tx %s terminate-private-plan 1 --from mykey
`,
version.AppName, types.ModuleName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

planId, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid plan id: %w", err)
}

msg := types.NewMsgTerminatePrivatePlan(clientCtx.GetFromAddress(), planId)
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}

func NewFarmCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "farm [coin]",
Expand Down
3 changes: 3 additions & 0 deletions x/lpfarm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func NewHandler(k keeper.Keeper) sdk.Handler {
case *types.MsgCreatePrivatePlan:
res, err := msgServer.CreatePrivatePlan(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgTerminatePrivatePlan:
res, err := msgServer.TerminatePrivatePlan(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgFarm:
res, err := msgServer.Farm(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
Expand Down
25 changes: 25 additions & 0 deletions x/lpfarm/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/crescent-network/crescent/v4/x/lpfarm/types"
)
Expand Down Expand Up @@ -41,6 +42,30 @@ func (k msgServer) CreatePrivatePlan(goCtx context.Context, msg *types.MsgCreate
}, nil
}

// TerminatePrivatePlan defines a method to terminate a private plan.
func (k msgServer) TerminatePrivatePlan(goCtx context.Context, msg *types.MsgTerminatePrivatePlan) (*types.MsgTerminatePrivatePlanResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

plan, found := k.GetPlan(ctx, msg.PlanId)
if !found {
return nil, sdkerrors.Wrapf(sdkerrors.ErrNotFound, "plan not found: %d", msg.PlanId)
}
if !plan.IsPrivate {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "cannot terminate public plan")
}
if plan.TerminationAddress != msg.Creator {
return nil, sdkerrors.Wrapf(
sdkerrors.ErrUnauthorized,
"plan's termination address must be same with the sender's address")
}

if err := k.Keeper.TerminatePlan(ctx, plan); err != nil {
return nil, err
}

return &types.MsgTerminatePrivatePlanResponse{}, nil
}

// Farm defines a method for farming coins.
func (k msgServer) Farm(goCtx context.Context, msg *types.MsgFarm) (*types.MsgFarmResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
Expand Down
51 changes: 51 additions & 0 deletions x/lpfarm/keeper/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package keeper_test

import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

utils "github.com/crescent-network/crescent/v4/types"
"github.com/crescent-network/crescent/v4/x/lpfarm/keeper"
"github.com/crescent-network/crescent/v4/x/lpfarm/types"
)

Expand Down Expand Up @@ -61,6 +63,55 @@ func (s *KeeperTestSuite) TestCreatePrivatePlan_PairNotFound() {
err, "pair 2 not found: not found")
}

func (s *KeeperTestSuite) TestTerminatePrivatePlan() {
s.createPairWithLastPrice("denom1", "denom2", sdk.NewDec(1))
s.createPool(1, utils.ParseCoins("100_000000denom1,100_000000denom2"))

creatorAddr := utils.TestAddress(1)
s.fundAddr(creatorAddr, s.keeper.GetPrivatePlanCreationFee(s.ctx))
plan, err := s.keeper.CreatePrivatePlan(
s.ctx, creatorAddr, "", []types.RewardAllocation{
types.NewPairRewardAllocation(1, utils.ParseCoins("100_000000stake")),
}, sampleStartTime, sampleEndTime)
s.Require().NoError(err)
s.fundAddr(plan.GetFarmingPoolAddress(), utils.ParseCoins("10000_000000stake"))

s.farm(utils.TestAddress(2), utils.ParseCoin("1000000pool1"))

s.nextBlock()
s.nextBlock()
s.nextBlock()

balancesBefore := s.getBalances(creatorAddr)
remainingFarmingRewards := s.getBalances(plan.GetFarmingPoolAddress())

msgServer := keeper.NewMsgServerImpl(s.keeper)
msg := types.NewMsgTerminatePrivatePlan(creatorAddr, 1)
_, err = msgServer.TerminatePrivatePlan(sdk.WrapSDKContext(s.ctx), msg)
s.Require().NoError(err)

s.assertEq(sdk.Coins{}, s.getBalances(plan.GetFarmingPoolAddress()))
s.assertEq(balancesBefore.Add(remainingFarmingRewards...), s.getBalances(creatorAddr))
}

func (s *KeeperTestSuite) TestTerminatePrivatePlan_Unauthorized() {
s.createPairWithLastPrice("denom1", "denom2", sdk.NewDec(1))

creatorAddr := utils.TestAddress(1)
s.fundAddr(creatorAddr, s.keeper.GetPrivatePlanCreationFee(s.ctx))
plan, err := s.keeper.CreatePrivatePlan(
s.ctx, creatorAddr, "", []types.RewardAllocation{
types.NewPairRewardAllocation(1, utils.ParseCoins("100_000000stake")),
}, sampleStartTime, sampleEndTime)
s.Require().NoError(err)
s.fundAddr(plan.GetFarmingPoolAddress(), utils.ParseCoins("10000_000000stake"))

msgServer := keeper.NewMsgServerImpl(s.keeper)
msg := types.NewMsgTerminatePrivatePlan(utils.TestAddress(2), 1)
_, err = msgServer.TerminatePrivatePlan(sdk.WrapSDKContext(s.ctx), msg)
s.Require().ErrorIs(err, sdkerrors.ErrUnauthorized)
}

func (s *KeeperTestSuite) TestAllocateRewards_NoFarmer() {
s.createPairWithLastPrice("denom1", "denom2", sdk.NewDec(1))
s.createPool(1, utils.ParseCoins("100_000000denom1,100_000000denom2"))
Expand Down
83 changes: 71 additions & 12 deletions x/lpfarm/simulation/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ import (

// Simulation operation weights constants.
const (
OpWeightMsgCreatePrivatePlan = "op_weight_msg_create_private_plan"
OpWeightMsgFarm = "op_weight_msg_farm"
OpWeightMsgUnfarm = "op_weight_msg_unfarm"
OpWeightMsgHarvest = "op_weight_msg_harvest"
OpWeightMsgCreatePrivatePlan = "op_weight_msg_create_private_plan"
OpWeightMsgTerminatePrivatePlan = "op_weight_msg_terminate_private_plan"
OpWeightMsgFarm = "op_weight_msg_farm"
OpWeightMsgUnfarm = "op_weight_msg_unfarm"
OpWeightMsgHarvest = "op_weight_msg_harvest"

DefaultWeightCreatePrivatePlan = 10
DefaultWeightFarm = 40
DefaultWeightUnfarm = 50
DefaultWeightHarvest = 20
DefaultWeightCreatePrivatePlan = 10
DefaultWeightTerminatePrivatePlan = 5
DefaultWeightFarm = 40
DefaultWeightUnfarm = 50
DefaultWeightHarvest = 20
)

var (
Expand All @@ -43,14 +45,18 @@ func WeightedOperations(
ak types.AccountKeeper, bk types.BankKeeper, lk types.LiquidityKeeper, k keeper.Keeper,
) simulation.WeightedOperations {
var (
weightMsgCreatePrivatePlan int
weightMsgFarm int
weightMsgUnfarm int
weightMsgHarvest int
weightMsgCreatePrivatePlan int
weightMsgTerminatePrivatePlan int
weightMsgFarm int
weightMsgUnfarm int
weightMsgHarvest int
)
appParams.GetOrGenerate(cdc, OpWeightMsgCreatePrivatePlan, &weightMsgCreatePrivatePlan, nil, func(_ *rand.Rand) {
weightMsgCreatePrivatePlan = DefaultWeightCreatePrivatePlan
})
appParams.GetOrGenerate(cdc, OpWeightMsgTerminatePrivatePlan, &weightMsgTerminatePrivatePlan, nil, func(_ *rand.Rand) {
weightMsgTerminatePrivatePlan = DefaultWeightTerminatePrivatePlan
})
appParams.GetOrGenerate(cdc, OpWeightMsgFarm, &weightMsgFarm, nil, func(_ *rand.Rand) {
weightMsgFarm = DefaultWeightFarm
})
Expand All @@ -66,6 +72,10 @@ func WeightedOperations(
weightMsgCreatePrivatePlan,
SimulateMsgCreatePrivatePlan(ak, bk, lk, k),
),
simulation.NewWeightedOperation(
weightMsgTerminatePrivatePlan,
SimulateMsgTerminatePrivatePlan(ak, bk, k),
),
simulation.NewWeightedOperation(
weightMsgFarm,
SimulateMsgFarm(ak, bk),
Expand Down Expand Up @@ -137,6 +147,55 @@ func SimulateMsgCreatePrivatePlan(
}
}

func SimulateMsgTerminatePrivatePlan(
ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper,
) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simtypes.Account, chainID string,
) (simtypes.OperationMsg, []simtypes.FutureOperation, error) {
accs = utils.ShuffleSimAccounts(r, accs)
var simAccount simtypes.Account
var planId uint64
skip := true
for _, simAccount = range accs {
k.IterateAllPlans(ctx, func(plan types.Plan) (stop bool) {
if !plan.IsTerminated && plan.GetTerminationAddress().Equals(simAccount.Address) {
planId = plan.Id
return true
}
return false
})
if planId > 0 {
skip = false
break
}
}
if skip {
return simtypes.NoOpMsg(
types.ModuleName, types.TypeMsgFarm, "no account to farm"), nil, nil
}

msg := types.NewMsgTerminatePrivatePlan(simAccount.Address, planId)

txCtx := simulation.OperationInput{
R: r,
App: app,
TxGen: appparams.MakeTestEncodingConfig().TxConfig,
Msg: msg,
MsgType: msg.Type(),
Context: ctx,
SimAccount: simAccount,
AccountKeeper: ak,
Bankkeeper: bk,
ModuleName: types.ModuleName,
CoinsSpentInMsg: bk.SpendableCoins(ctx, simAccount.Address),
}

return utils.GenAndDeliverTxWithFees(txCtx, gas, fees)
}
}

func SimulateMsgFarm(ak types.AccountKeeper, bk types.BankKeeper) simtypes.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
Expand Down
Loading

0 comments on commit 89de396

Please sign in to comment.