diff --git a/x/hard/client/cli/tx.go b/x/hard/client/cli/tx.go index 3db90bfe..85874f95 100644 --- a/x/hard/client/cli/tx.go +++ b/x/hard/client/cli/tx.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" @@ -33,13 +34,20 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { getCmdDeposit(cdc), getCmdWithdraw(cdc), getCmdBorrow(cdc), + addOptionalFlag(getCmdRepay(cdc), flagOwner, "", "original borrower's address whose loan will be repaid"), getCmdLiquidate(cdc), - getCmdRepay(cdc), )...) return hardTxCmd } +// addFlag adds a cobra flag and binds it using viper +func addOptionalFlag(cmd *cobra.Command, flagName, flagValue, flagUsage string) *cobra.Command { + cmd.Flags().String(flagName, flagValue, flagUsage) + viper.BindPFlag(flagName, cmd.Flags().Lookup(flagName)) + return cmd +} + func getCmdDeposit(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "deposit [amount]", @@ -91,7 +99,7 @@ func getCmdWithdraw(cdc *codec.Codec) *cobra.Command { func getCmdBorrow(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "borrow [1000000000ukava]", + Use: "borrow [amount]", Short: "borrow tokens from the hard protocol", Long: strings.TrimSpace(`borrows tokens from the hard protocol`), Args: cobra.ExactArgs(1), @@ -119,24 +127,40 @@ func getCmdBorrow(cdc *codec.Codec) *cobra.Command { func getCmdRepay(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "repay [1000000000ukava]", + Use: "repay [amount]", Short: "repay tokens to the hard protocol", - Long: strings.TrimSpace(`repay tokens to the hard protocol`), + Long: strings.TrimSpace(`repay tokens to the hard protocol with optional --owner param to repay another account's loan`), Args: cobra.ExactArgs(1), - Example: fmt.Sprintf( - `%s tx %s repay 1000000000ukava,25000000000bnb --from `, version.ClientName, types.ModuleName, - ), + Example: strings.TrimSpace(` +kvcli tx hard repay 1000000000ukava --from +kvcli tx hard repay 1000000000ukava,25000000000bnb --from +kvcli tx hard repay 1000000000ukava,25000000000bnb --owner --from + `), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + var owner sdk.AccAddress + ownerStr := viper.GetString(flagOwner) + + // Parse optional owner argument or default to sender + if len(ownerStr) > 0 { + ownerAddr, err := sdk.AccAddressFromBech32(ownerStr) + if err != nil { + return err + } + owner = ownerAddr + } else { + owner = cliCtx.GetFromAddress() + } + coins, err := sdk.ParseCoins(args[0]) if err != nil { return err } - msg := types.NewMsgRepay(cliCtx.GetFromAddress(), coins) + msg := types.NewMsgRepay(cliCtx.GetFromAddress(), owner, coins) if err := msg.ValidateBasic(); err != nil { return err } diff --git a/x/hard/client/rest/rest.go b/x/hard/client/rest/rest.go index 8d7a1659..a2752ff6 100644 --- a/x/hard/client/rest/rest.go +++ b/x/hard/client/rest/rest.go @@ -47,6 +47,7 @@ type PostBorrowReq struct { type PostRepayReq struct { BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` From sdk.AccAddress `json:"from" yaml:"from"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` Amount sdk.Coins `json:"amount" yaml:"amount"` } diff --git a/x/hard/client/rest/tx.go b/x/hard/client/rest/tx.go index 355cf43c..d3aae42c 100644 --- a/x/hard/client/rest/tx.go +++ b/x/hard/client/rest/tx.go @@ -97,7 +97,12 @@ func postRepayHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - msg := types.NewMsgRepay(req.From, req.Amount) + owner := req.Owner + if owner.Empty() { + owner = req.From + } + + msg := types.NewMsgRepay(req.From, owner, req.Amount) if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/hard/handler.go b/x/hard/handler.go index a8cfd7c3..ac4ad8fb 100644 --- a/x/hard/handler.go +++ b/x/hard/handler.go @@ -84,7 +84,7 @@ func handleMsgBorrow(ctx sdk.Context, k keeper.Keeper, msg types.MsgBorrow) (*sd } func handleMsgRepay(ctx sdk.Context, k keeper.Keeper, msg types.MsgRepay) (*sdk.Result, error) { - err := k.Repay(ctx, msg.Sender, msg.Amount) + err := k.Repay(ctx, msg.Sender, msg.Owner, msg.Amount) if err != nil { return nil, err } diff --git a/x/hard/keeper/repay.go b/x/hard/keeper/repay.go index 50ac19a6..ee6b0a39 100644 --- a/x/hard/keeper/repay.go +++ b/x/hard/keeper/repay.go @@ -8,9 +8,9 @@ import ( ) // Repay borrowed funds -func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error { +func (k Keeper) Repay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.Coins) error { // Check borrow exists here to avoid duplicating store read in ValidateRepay - borrow, found := k.GetBorrow(ctx, sender) + borrow, found := k.GetBorrow(ctx, owner) if !found { return types.ErrBorrowNotFound } @@ -18,10 +18,10 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e k.BeforeBorrowModified(ctx, borrow) // Sync borrow interest so loan is up-to-date - k.SyncBorrowInterest(ctx, sender) + k.SyncBorrowInterest(ctx, owner) - // Validate requested repay - err := k.ValidateRepay(ctx, sender, coins) + // Validate that sender holds coins for repayment + err = k.ValidateRepay(ctx, sender, coins) if err != nil { return err } @@ -69,6 +69,7 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e sdk.NewEvent( types.EventTypeHardRepay, sdk.NewAttribute(types.AttributeKeySender, sender.String()), + sdk.NewAttribute(types.AttributeKeyOwner, owner.String()), sdk.NewAttribute(types.AttributeKeyRepayCoins, payment.String()), ), ) diff --git a/x/hard/keeper/repay_test.go b/x/hard/keeper/repay_test.go index b83df3b7..159edd26 100644 --- a/x/hard/keeper/repay_test.go +++ b/x/hard/keeper/repay_test.go @@ -211,7 +211,7 @@ func (suite *KeeperTestSuite) TestRepay() { err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins) suite.Require().NoError(err) - err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.repayCoins) + err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.repayCoins) if tc.errArgs.expectPass { suite.Require().NoError(err) // If we overpaid expect an adjustment diff --git a/x/hard/keeper/withdraw_test.go b/x/hard/keeper/withdraw_test.go index 99b3781c..80a40ba1 100644 --- a/x/hard/keeper/withdraw_test.go +++ b/x/hard/keeper/withdraw_test.go @@ -356,7 +356,7 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() { suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) // Repay the initial principal - err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrowCoins) + err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.borrowCoins) suite.Require().NoError(err) // Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt diff --git a/x/hard/types/events.go b/x/hard/types/events.go index d47c1a64..6db08778 100644 --- a/x/hard/types/events.go +++ b/x/hard/types/events.go @@ -26,4 +26,5 @@ const ( AttributeKeyLiquidatedCoins = "liquidated_coins" AttributeKeyKeeper = "keeper" AttributeKeyKeeperRewardCoins = "keeper_reward_coins" + AttributeKeyOwner = "owner" ) diff --git a/x/hard/types/msg.go b/x/hard/types/msg.go index b76398b1..adbb55e5 100644 --- a/x/hard/types/msg.go +++ b/x/hard/types/msg.go @@ -169,13 +169,15 @@ func (msg MsgBorrow) String() string { // MsgRepay repays funds to the hard module. type MsgRepay struct { Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` Amount sdk.Coins `json:"amount" yaml:"amount"` } // NewMsgRepay returns a new MsgRepay -func NewMsgRepay(sender sdk.AccAddress, amount sdk.Coins) MsgRepay { +func NewMsgRepay(sender, owner sdk.AccAddress, amount sdk.Coins) MsgRepay { return MsgRepay{ Sender: sender, + Owner: owner, Amount: amount, } } @@ -191,6 +193,9 @@ func (msg MsgRepay) ValidateBasic() error { if msg.Sender.Empty() { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty") } + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } if !msg.Amount.IsValid() || msg.Amount.IsZero() { return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "repay amount %s", msg.Amount) } @@ -212,8 +217,9 @@ func (msg MsgRepay) GetSigners() []sdk.AccAddress { func (msg MsgRepay) String() string { return fmt.Sprintf(`Repay Message: Sender: %s + Owner: %s Amount: %s -`, msg.Sender, msg.Amount) +`, msg.Sender, msg.Owner, msg.Amount) } // MsgLiquidate attempts to liquidate a borrower's borrow diff --git a/x/hard/types/msg_test.go b/x/hard/types/msg_test.go index b9ed9580..467bcf54 100644 --- a/x/hard/types/msg_test.go +++ b/x/hard/types/msg_test.go @@ -152,6 +152,7 @@ func (suite *MsgTestSuite) TestMsgBorrow() { func (suite *MsgTestSuite) TestMsgRepay() { type args struct { sender sdk.AccAddress + owner sdk.AccAddress amount sdk.Coins } addrs := []sdk.AccAddress{ @@ -167,6 +168,7 @@ func (suite *MsgTestSuite) TestMsgRepay() { name: "valid", args: args{ sender: addrs[0], + owner: addrs[0], amount: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000000))), }, expectPass: true, @@ -175,7 +177,7 @@ func (suite *MsgTestSuite) TestMsgRepay() { } for _, tc := range testCases { suite.Run(tc.name, func() { - msg := types.NewMsgRepay(tc.args.sender, tc.args.amount) + msg := types.NewMsgRepay(tc.args.sender, tc.args.owner, tc.args.amount) err := msg.ValidateBasic() if tc.expectPass { suite.NoError(err)