diff --git a/swagger-ui/swagger.yaml b/swagger-ui/swagger.yaml index e320fb7a..e9e86b00 100644 --- a/swagger-ui/swagger.yaml +++ b/swagger-ui/swagger.yaml @@ -410,6 +410,8 @@ $ref: '#/definitions/CoinCollateral' principal: $ref: '#/definitions/CoinPrincipal' + collateral_type: + $ref: '#/definitions/CollateralType' responses: 200: description: Tx was successfully generated @@ -419,7 +421,7 @@ description: Invalid request 500: description: Server internal error - /cdp/{owner}/{denom}/deposits: + /cdp/{owner}/{collateral-type}/deposits: post: summary: Create a deposit to cdp transaction tags: @@ -436,11 +438,11 @@ type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path - name: denom - description: Collateral denom + name: collateral_type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a - description: deposit cdp post parameters name: post_deposit_req in: body @@ -456,6 +458,8 @@ $ref: '#/definitions/Address' collateral: $ref: '#/definitions/CoinCollateral' + collateral_type: + $ref: '#/definitions/CollateralType' responses: 200: description: Tx was successfully generated @@ -465,7 +469,7 @@ description: Invalid request 500: description: Server internal error - /cdp/{owner}/{denom}/withdraw: + /cdp/{owner}/{collateral-type}/withdraw: post: summary: create a withdraw collateral transaction tags: @@ -482,11 +486,11 @@ type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path - name: denom - description: Collateral denom + name: collateral_type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a - description: withdraw cdp post parameters name: post_withdraw_req in: body @@ -502,6 +506,8 @@ $ref: '#/definitions/Address' collateral: $ref: '#/definitions/CoinCollateral' + collateral_type: + $ref: '#/definitions/CollateralType' responses: 200: description: Tx was successfully generated @@ -511,7 +517,7 @@ description: Invalid request 500: description: Server internal error - /cdp/{owner}/{denom}/draw: + /cdp/{owner}/{collateral-type}/draw: post: summary: Create a draw debt transaction tags: @@ -528,11 +534,11 @@ type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path - name: denom - description: Collateral denom + name: collateral_type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a - description: draw cdp post parameters name: post_draw_req in: body @@ -546,6 +552,8 @@ $ref: '#/definitions/Address' principal: $ref: '#/definitions/CoinPrincipal' + collateral_type: + $ref: '#/definitions/CollateralType' responses: 200: description: Tx was successfully generated @@ -555,7 +563,7 @@ description: Invalid request 500: description: Server internal error - /cdp/{owner}/{denom}/repay: + /cdp/{owner}/{collateral-type}/repay: post: summary: Repay debt from a CDP tags: @@ -571,12 +579,12 @@ required: true type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - - in: path - name: denom - description: Collateral denom + - in: path + name: collateral_type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a - description: repay cdp post parameters name: post_repay_req in: body @@ -590,6 +598,8 @@ $ref: '#/definitions/Address' payment: $ref: '#/definitions/CoinPrincipal' + collateral_type: + $ref: '#/definitions/CollateralType' responses: 200: description: Tx was successfully generated @@ -640,7 +650,6 @@ type: number 500: description: Server internal error - /cdp/parameters: get: summary: Get the parameters of the cdp module @@ -669,17 +678,26 @@ surplus_auction_threshold: type: string example: '1000000000' + surplus_auction_lot: + type: string + example: '10000000' debt_auction_threshold: type: string example: '1000000000' + surplus_auction_lot: + type: string + example: '10000000' + savings_distribution_frequency: + type: string + example: '60000000' circuit_breaker: type: boolean example: false 500: description: Server internal error - /cdp/cdps/cdp/{owner}/{denom}: + /cdp/cdps/cdp/{owner}/{collateral-type}: get: - summary: Get the cdp associated with the input owner address and collateral denom + summary: Get the cdp associated with the input owner address and collateral type tags: - CDP produces: @@ -692,11 +710,11 @@ type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path - name: denom - description: Collateral denom + name: type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a responses: 200: description: CDP associated with owner @@ -704,23 +722,23 @@ $ref: '#/definitions/CdpResponse' 500: description: Server internal error - /cdp/cdps/denom/{denom}: + /cdp/cdps/denom/{collateral-type}: get: - summary: Get all cdps with collateral type equal to the input collateral denom + summary: Get all cdps with collateral type equal to the input collateral type tags: - CDP produces: - application/json parameters: - in: path - name: denom - description: Collateral denom + name: type + description: Collateral Type required: true type: string - x-example: xrp + x-example: xrp-a responses: 200: - description: All CDPs with the input collateral denom + description: All CDPs with the input collateral type schema: type: object properties: @@ -734,20 +752,20 @@ $ref: '#/definitions/CdpResponse' 500: description: Server internal error - /cdp/cdps/ratio/{denom}/{ratio}: + /cdp/cdps/ratio/{collateral-type}/{ratio}: get: - summary: Get all cdps with collateral type equal to the input collateral denom and collateralization ratio strictly less than the input ratio + summary: Get all cdps with collateral type equal to the input collateral type and collateralization ratio strictly less than the input ratio tags: - CDP produces: - application/json parameters: - in: path - name: denom - description: Collateral denom + name: type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a - in: path name: ratio description: Collateralization ratio @@ -756,7 +774,7 @@ x-example: "2.0" responses: 200: - description: All CDPs with the input collateral denom and collateralization ratio less than the input ratio + description: All CDPs with the input collateral type and collateralization ratio less than the input ratio schema: type: object properties: @@ -770,9 +788,9 @@ $ref: '#/definitions/CdpResponse' 500: description: Server internal error - /cdp/cdps/cdp/deposits/{owner}/{denom}: + /cdp/cdps/cdp/deposits/{owner}/{collateral-type}: get: - summary: Get the deposits associated with the cdp owned by the input owner address and with collateral type equal to the input collateral denom + summary: Get the deposits associated with the cdp owned by an address for a collateral type tags: - CDP produces: @@ -785,11 +803,11 @@ type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path - name: denom - description: Collateral denom + name: type + description: Collateral type required: true type: string - x-example: xrp + x-example: xrp-a responses: 200: description: Deposits associated with the cdp @@ -805,6 +823,73 @@ $ref: '#/definitions/CdpDepositResponse' 500: description: Server internal error + /cdp/savings-rate-dist: + get: + summary: Get the total savings rate distributed by the cdp module + tags: + - CDP + produces: + - application/json + responses: + 200: + description: The distributed savings rate + properties: + height: + type: string + example: "100" + result: + savings_rate_distributed: + type: string + example: "5000000000000" + 500: + description: Server internal error + /cdp/cdps: + get: + summary: Query all active cdps + tags: + - CDP + produces: + - application/json + parameters: + - in: query + name: owner + description: Owner address in bech32 format + required: false + type: string + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c + - in: query + name: collateral-type + description: Collateral type + required: false + type: string + x-example: xrp-a + - in: query + name: id + description: CDP ID + required: false + type: string + x-example: "4" + - in: query + name: ratio + description: Collateralization ratio + required: false + type: string + x-example: "2.75" + responses: + 200: + description: Query cdps + schema: + type: object + properties: + height: + type: string + example: "100" + result: + type: array + items: + $ref: '#/definitions/CdpResponse' + 500: + description: Internal Server Error /bep3/swap/create: post: summary: Generate a create atomic swap transaction @@ -1273,7 +1358,7 @@ $ref: '#/definitions/BaseReq' owner: $ref: '#/definitions/Address' - denom: + type: type: string example: bnb responses: @@ -1285,9 +1370,9 @@ description: Invalid request 500: description: Internal server error - /incentive/claims/{owner}/{denom}: + /incentive/claims/{owner}/{collateral-type}: get: - summary: Get outstanding claims for the input owner and denom + summary: Get outstanding claims for the input owner and collateral type tags: - Incentive produces: @@ -1299,11 +1384,11 @@ type: string x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path - name: denom - description: Collateral denom + name: type + description: Collateral type required: true type: string - x-example: bnb + x-example: bnb-a responses: 200: description: USDX Incentive Claims @@ -3457,6 +3542,9 @@ amount: type: string example: '555555' + CollateralType: + type: string + example: xrp-a CoinCollateral: type: object properties: @@ -3904,6 +3992,9 @@ example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 collateral: $ref: '#/definitions/CoinCollateral' + type: + type: string + example: "xrp-a" principal: $ref: '#/definitions/CoinPrincipal' accumulated_fees: @@ -3987,6 +4078,15 @@ bid_duration: type: string example: 600000000000 + increment_surplus: + type: string + example: "0.05" + increment_debt: + type: string + example: "0.05" + increment_collateral: + type: string + example: "0.05" AuctionResponse: type: object properties: diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go index 97c4ce6c..2bbc16eb 100644 --- a/x/auction/client/cli/query.go +++ b/x/auction/client/cli/query.go @@ -22,6 +22,7 @@ const ( flagType = "type" flagDenom = "denom" flagPhase = "phase" + flagOwner = "owner" ) // GetQueryCmd returns the cli query commands for this module @@ -77,6 +78,7 @@ func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { Long: strings.TrimSpace(`Query for all paginated auctions that match optional filters: Example: $ kvcli q auction auctions --type=(collateral|surplus|debt) +$ kvcli q auction auctions --owner=kava1hatdq32u5x4wnxrtv5wzjzmq49sxgjgsj0mffm $ kvcli q auction auctions --denom=bnb $ kvcli q auction auctions --phase=(forward|reverse) $ kvcli q auction auctions --page=2 --limit=100 @@ -84,6 +86,7 @@ $ kvcli q auction auctions --page=2 --limit=100 ), RunE: func(cmd *cobra.Command, args []string) error { strType := viper.GetString(flagType) + strOwner := viper.GetString(flagOwner) strDenom := viper.GetString(flagDenom) strPhase := viper.GetString(flagPhase) page := viper.GetInt(flags.FlagPage) @@ -91,11 +94,12 @@ $ kvcli q auction auctions --page=2 --limit=100 var ( auctionType string + auctionOwner sdk.AccAddress auctionDenom string auctionPhase string ) - params := types.NewQueryAllAuctionParams(page, limit, auctionType, auctionDenom, auctionPhase) + params := types.NewQueryAllAuctionParams(page, limit, auctionType, auctionDenom, auctionPhase, auctionOwner) if len(strType) != 0 { auctionType = strings.ToLower(strings.TrimSpace(strType)) @@ -107,6 +111,18 @@ $ kvcli q auction auctions --page=2 --limit=100 params.Type = auctionType } + if len(auctionOwner) != 0 { + if auctionType != types.CollateralAuctionType { + return fmt.Errorf("cannot apply owner flag to non-collateral auction type") + } + auctionOwnerStr := strings.ToLower(strings.TrimSpace(strOwner)) + auctionOwner, err := sdk.AccAddressFromBech32(auctionOwnerStr) + if err != nil { + return fmt.Errorf("cannot parse address from auction owner %s", auctionOwnerStr) + } + params.Owner = auctionOwner + } + if len(strDenom) != 0 { auctionDenom := strings.TrimSpace(strDenom) err := sdk.ValidateDenom(auctionDenom) @@ -160,6 +176,7 @@ $ kvcli q auction auctions --page=2 --limit=100 cmd.Flags().Int(flags.FlagPage, 1, "pagination page of auctions to to query for") cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit of auctions to query for") cmd.Flags().String(flagType, "", "(optional) filter by auction type, type: collateral, debt, surplus") + cmd.Flags().String(flagOwner, "", "(optional) filter by collateral auction owner") cmd.Flags().String(flagDenom, "", "(optional) filter by auction denom") cmd.Flags().String(flagPhase, "", "(optional) filter by collateral auction phase, phase: forward/reverse") diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go index a3a9ef17..2a8868ad 100644 --- a/x/auction/client/rest/query.go +++ b/x/auction/client/rest/query.go @@ -69,6 +69,7 @@ func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } var auctionType string + var auctionOwner sdk.AccAddress var auctionDenom string var auctionPhase string @@ -82,6 +83,17 @@ func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } + if x := r.URL.Query().Get(RestOwner); len(x) != 0 { + if auctionType != types.CollateralAuctionType { + rest.WriteErrorResponse(w, http.StatusBadRequest, "cannot apply owner flag to non-collateral auction type") + } + auctionOwnerStr := strings.ToLower(strings.TrimSpace(x)) + auctionOwner, err = sdk.AccAddressFromBech32(auctionOwnerStr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from auction owner %s", auctionOwnerStr)) + } + } + if x := r.URL.Query().Get(RestDenom); len(x) != 0 { auctionDenom = strings.TrimSpace(x) err := sdk.ValidateDenom(auctionDenom) @@ -103,7 +115,7 @@ func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } - params := types.NewQueryAllAuctionParams(page, limit, auctionType, auctionDenom, auctionPhase) + params := types.NewQueryAllAuctionParams(page, limit, auctionType, auctionDenom, auctionPhase, auctionOwner) bz, err := cliCtx.Codec.MarshalJSON(params) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) diff --git a/x/auction/client/rest/rest.go b/x/auction/client/rest/rest.go index 4667f777..08bd2e3f 100644 --- a/x/auction/client/rest/rest.go +++ b/x/auction/client/rest/rest.go @@ -12,6 +12,7 @@ import ( // nolint const ( RestType = "type" + RestOwner = "owner" RestDenom = "denom" RestPhase = "phase" ) diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index afb8598a..71cc9320 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -1,12 +1,11 @@ package keeper import ( - abci "github.com/tendermint/tendermint/abci/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/x/auction/types" ) @@ -93,13 +92,29 @@ func filterAuctions(ctx sdk.Context, auctions types.Auctions, params types.Query filteredAuctions := make(types.Auctions, 0, len(auctions)) for _, auc := range auctions { - matchType, matchDenom, matchPhase := true, true, true + matchType, matchOwner, matchDenom, matchPhase := true, true, true, true // match auction type (if supplied) if len(params.Type) > 0 { matchType = auc.GetType() == params.Type } + // match auction owner (if supplied) + if len(params.Owner) > 0 { + if cAuc, ok := auc.(types.CollateralAuction); ok { + foundOwnerAddr := false + for _, addr := range cAuc.GetLotReturns().Addresses { + if addr.Equals(params.Owner) { + foundOwnerAddr = true + break + } + } + if !foundOwnerAddr { + matchOwner = false + } + } + } + // match auction denom (if supplied) if len(params.Denom) > 0 { matchDenom = auc.GetBid().Denom == params.Denom || auc.GetLot().Denom == params.Denom @@ -110,7 +125,7 @@ func filterAuctions(ctx sdk.Context, auctions types.Auctions, params types.Query matchPhase = auc.GetPhase() == params.Phase } - if matchType && matchDenom && matchPhase { + if matchType && matchOwner && matchDenom && matchPhase { filteredAuctions = append(filteredAuctions, auc) } } diff --git a/x/auction/keeper/querier_test.go b/x/auction/keeper/querier_test.go index 8d74ee10..6ca433a1 100644 --- a/x/auction/keeper/querier_test.go +++ b/x/auction/keeper/querier_test.go @@ -41,7 +41,7 @@ func (suite *QuerierTestSuite) SetupTest() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) - _, addrs := app.GeneratePrivKeyAddressPairs(1) + _, addrs := app.GeneratePrivKeyAddressPairs(10) buyer := addrs[0] modName := cdp.LiquidatorMacc @@ -64,8 +64,16 @@ func (suite *QuerierTestSuite) SetupTest() { // Populate with auctions randSrc := rand.New(rand.NewSource(int64(1234))) for j := 0; j < TestAuctionCount; j++ { - lotAmount := simulation.RandIntBetween(randSrc, 10, 100) - id, err := suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", int64(lotAmount)), "token2") + var id uint64 + var err error + lotAmount := int64(simulation.RandIntBetween(randSrc, 10, 100)) + ownerAddrIndex := simulation.RandIntBetween(randSrc, 1, 9) + if ownerAddrIndex%2 == 0 { + id, err = suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", lotAmount), "token2") + } else { + id, err = suite.keeper.StartCollateralAuction(suite.ctx, modName, c("token1", lotAmount), c("usdx", int64(20)), + []sdk.AccAddress{addrs[ownerAddrIndex]}, []sdk.Int{sdk.NewInt(lotAmount)}, c("debt", int64(10))) + } suite.NoError(err) auc, found := suite.keeper.GetAuction(suite.ctx, id) @@ -108,7 +116,7 @@ func (suite *QuerierTestSuite) TestQueryAuctions() { query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuctions}, "/"), Data: types.ModuleCdc.MustMarshalJSON( - types.NewQueryAllAuctionParams(int(1), int(TestAuctionCount), "", "", ""), + types.NewQueryAllAuctionParams(int(1), int(TestAuctionCount), "", "", "", nil), ), } diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 4c413913..95ec1095 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -244,6 +244,11 @@ func (a CollateralAuction) GetPhase() string { return ForwardAuctionPhase } +// GetLotReturns returns a collateral auction's lot owners +func (a CollateralAuction) GetLotReturns() WeightedAddresses { + return a.LotReturns +} + // Validate validates the CollateralAuction fields values. func (a CollateralAuction) Validate() error { if !a.CorrespondingDebt.IsValid() { diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 1fbdc711..a409de2d 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -1,5 +1,9 @@ package types +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + const ( // QueryGetAuction is the query path for querying one auction QueryGetAuction = "auction" @@ -25,19 +29,21 @@ func NewQueryAuctionParams(id uint64) QueryAuctionParams { // QueryAllAuctionParams is the params for an auctions query type QueryAllAuctionParams struct { - Page int `json:"page" yaml:"page"` - Limit int `json:"limit" yaml:"limit"` - Type string `json:"type" yaml:"type"` - Denom string `json:"denom" yaml:"denom"` - Phase string `json:"phase" yaml:"phase"` + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` + Type string `json:"type" yaml:"type"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Denom string `json:"denom" yaml:"denom"` + Phase string `json:"phase" yaml:"phase"` } // NewQueryAllAuctionParams creates a new QueryAllAuctionParams -func NewQueryAllAuctionParams(page, limit int, aucType, aucDenom, aucPhase string) QueryAllAuctionParams { +func NewQueryAllAuctionParams(page, limit int, aucType, aucDenom, aucPhase string, aucOwner sdk.AccAddress) QueryAllAuctionParams { return QueryAllAuctionParams{ Page: page, Limit: limit, Type: aucType, + Owner: aucOwner, Denom: aucDenom, Phase: aucPhase, } diff --git a/x/cdp/alias.go b/x/cdp/alias.go index a805372d..600e5232 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -137,7 +137,9 @@ var ( DefaultDebtLot = types.DefaultDebtLot DefaultPreviousDistributionTime = types.DefaultPreviousDistributionTime DefaultSavingsDistributionFrequency = types.DefaultSavingsDistributionFrequency + DefaultSavingsRateDistributed = types.DefaultSavingsRateDistributed MaxSortableDec = types.MaxSortableDec + SavingsRateDistributedKey = types.SavingsRateDistributedKey ) type ( diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index d511ed2a..1cc68b18 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -2,9 +2,11 @@ package cli import ( "fmt" + "strconv" "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/flags" @@ -16,6 +18,14 @@ import ( "github.com/kava-labs/kava/x/cdp/types" ) +// Query CDP flags +const ( + flagCollateralType = "collateral-type" + flagOwner = "owner" + flagID = "id" + flagRatio = "ratio" // returns CDPs under the given collateralization ratio threshold +) + // GetQueryCmd returns the cli query commands for this module func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Group nameservice queries under a subcommand @@ -26,11 +36,11 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { cdpQueryCmd.AddCommand(flags.GetCommands( QueryCdpCmd(queryRoute, cdc), - QueryCdpsByCollateralTypeCmd(queryRoute, cdc), - QueryCdpsByCollateralTypeAndRatioCmd(queryRoute, cdc), + QueryGetCdpsCmd(queryRoute, cdc), QueryCdpDepositsCmd(queryRoute, cdc), QueryParamsCmd(queryRoute, cdc), QueryGetAccounts(queryRoute, cdc), + QueryGetSavingsRateDistributed(queryRoute, cdc), )...) return cdpQueryCmd @@ -79,85 +89,103 @@ $ %s query %s cdp kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw atom-a } } -// QueryCdpsByCollateralTypeCmd returns the command handler for querying cdps for a collateral type -func QueryCdpsByCollateralTypeCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "cdps [collateral-type]", - Short: "query CDPs by collateral", - Long: strings.TrimSpace( - fmt.Sprintf(`List all CDPs collateralized with the specified asset. - +// QueryGetCdpsCmd queries the cdps in the store +func QueryGetCdpsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "cdps", + Short: "query cdps with optional filters", + Long: strings.TrimSpace(`Query for all paginated cdps that match optional filters: Example: -$ %s query %s cdps atom-a -`, version.ClientName, types.ModuleName)), - Args: cobra.ExactArgs(1), +$ kvcli q cdp cdps --collateral-type=bnb +$ kvcli q cdp cdps --owner=kava1hatdq32u5x4wnxrtv5wzjzmq49sxgjgsj0mffm +$ kvcli q cdp cdps --id=21 +$ kvcli q cdp cdps --ratio=2.75 +$ kvcli q cdp cdps --page=2 --limit=100 +`, + ), RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) + strCollateralType := viper.GetString(flagCollateralType) + strOwner := viper.GetString(flagOwner) + strID := viper.GetString(flagID) + strRatio := viper.GetString(flagRatio) + page := viper.GetInt(flags.FlagPage) + limit := viper.GetInt(flags.FlagLimit) - // Prepare params for querier - bz, err := cdc.MarshalJSON(types.QueryCdpsParams{CollateralType: args[0]}) + var ( + cdpCollateralType string + cdpOwner sdk.AccAddress + cdpID uint64 + cdpRatio sdk.Dec + ) + + params := types.NewQueryCdpsParams(page, limit, cdpCollateralType, cdpOwner, cdpID, cdpRatio) + + if len(strCollateralType) != 0 { + cdpCollateralType = strings.ToLower(strings.TrimSpace(strCollateralType)) + params.CollateralType = cdpCollateralType + } + + if len(strOwner) != 0 { + cdpOwner, err := sdk.AccAddressFromBech32(strings.ToLower(strings.TrimSpace(strOwner))) + if err != nil { + return fmt.Errorf("cannot parse address from cdp owner %s", strOwner) + } + params.Owner = cdpOwner + } + + if len(strID) != 0 { + cdpID, err := strconv.ParseUint(strID, 10, 64) + if err != nil { + return fmt.Errorf("cannot parse cdp ID %s", strID) + } + params.ID = cdpID + } + + params.Ratio = sdk.ZeroDec() + if len(strRatio) != 0 { + cdpRatio, err := sdk.NewDecFromStr(strRatio) + if err != nil { + return fmt.Errorf("cannot parse cdp ratio %s", strRatio) + } + params.Ratio = cdpRatio + } else { + // Set to sdk.Dec(0) so that if not specified in params it doesn't panic when unmarshaled + params.Ratio = sdk.ZeroDec() + } + + bz, err := cdc.MarshalJSON(params) if err != nil { return err } + cliCtx := context.NewCLIContext().WithCodec(cdc) + // Query - route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps) - res, _, err := cliCtx.QueryWithData(route, bz) + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps), bz) if err != nil { return err } // Decode and print results - var cdps types.AugmentedCDPs - cdc.MustUnmarshalJSON(res, &cdps) - return cliCtx.PrintOutput(cdps) + var matchingCDPs types.AugmentedCDPs + cdc.MustUnmarshalJSON(res, &matchingCDPs) + if len(matchingCDPs) == 0 { + return fmt.Errorf("No matching CDPs found") + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(matchingCDPs) // nolint:errcheck }, } -} -// QueryCdpsByCollateralTypeAndRatioCmd returns the command handler for querying cdps -// that are under the specified collateral ratio -func QueryCdpsByCollateralTypeAndRatioCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "cdps-by-ratio [collateral-type] [collateralization-ratio]", - Short: "get cdps under a collateralization ratio", - Long: strings.TrimSpace( - fmt.Sprintf(`List all CDPs under a specified collateralization ratio. -Collateralization ratio is: collateral * price / debt. + cmd.Flags().Int(flags.FlagPage, 1, "pagination page of CDPs to to query for") + cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit of CDPs to query for") + cmd.Flags().String(flagCollateralType, "", "(optional) filter by CDP collateral type") + cmd.Flags().String(flagOwner, "", "(optional) filter by CDP owner") + cmd.Flags().String(flagID, "", "(optional) filter by CDP ID") + cmd.Flags().String(flagRatio, "", "(optional) filter by CDP collateralization ratio threshold") -Example: -$ %s query %s cdps-by-ratio atom-a 1.6 -`, version.ClientName, types.ModuleName)), - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) - - // Prepare params for querier - ratio, err := sdk.NewDecFromStr(args[1]) - if err != nil { - return err - } - bz, err := cdc.MarshalJSON(types.QueryCdpsByRatioParams{ - CollateralType: args[0], - Ratio: ratio, - }) - if err != nil { - return err - } - - // Query - route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdpsByCollateralization) - res, _, err := cliCtx.QueryWithData(route, bz) - if err != nil { - return err - } - - // Decode and print results - var cdps types.AugmentedCDPs - cdc.MustUnmarshalJSON(res, &cdps) - return cliCtx.PrintOutput(cdps) - }, - } + return cmd } // QueryCdpDepositsCmd returns the command handler for querying the deposits of a particular cdp @@ -208,7 +236,7 @@ func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "params", Short: "get the cdp module parameters", - Long: "Get the current global cdp module parameters.", + Long: "get the current global cdp module parameters.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -228,11 +256,12 @@ func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } } +// QueryGetAccounts queries CDP module accounts func QueryGetAccounts(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "accounts", - Short: "Get module accounts", - Long: "Get cdp module account addresses", + Short: "get module accounts", + Long: "get cdp module account addresses", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -253,3 +282,30 @@ func QueryGetAccounts(queryRoute string, cdc *codec.Codec) *cobra.Command { }, } } + +// QueryGetSavingsRateDistributed queries the total amount of savings rate distributed in USDX +func QueryGetSavingsRateDistributed(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "savings-rate-dist", + Short: "get total amount of savings rate distributed in USDX", + Long: "get total amount of savings rate distributed", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetSavingsRateDistributed), nil) + if err != nil { + return err + } + cliCtx = cliCtx.WithHeight(height) + + // Decode and print results + var out sdk.Int + if err := cdc.UnmarshalJSON(res, &out); err != nil { + return fmt.Errorf("failed to unmarshal sdk.Int: %w", err) + } + return cliCtx.PrintOutput(out) + }, + } +} diff --git a/x/cdp/client/rest/query.go b/x/cdp/client/rest/query.go index c4e3dd03..7774d8eb 100644 --- a/x/cdp/client/rest/query.go +++ b/x/cdp/client/rest/query.go @@ -3,6 +3,8 @@ package rest import ( "fmt" "net/http" + "strconv" + "strings" "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" @@ -16,9 +18,11 @@ import ( func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc("/cdp/accounts", getAccountsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc("/cdp/parameters", getParamsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/cdp/savingsRateDist", getSavingsRateDistributedHandler(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/cdp/cdps/cdp/{%s}/{%s}", types.RestOwner, types.RestCollateralType), queryCdpHandlerFn(cliCtx)).Methods("GET") - r.HandleFunc(fmt.Sprintf("/cdp/cdps/collateralType/{%s}", types.RestCollateralType), queryCdpsHandlerFn(cliCtx)).Methods("GET") - r.HandleFunc(fmt.Sprintf("/cdp/cdps/ratio/{%s}/{%s}", types.RestCollateralType, types.RestRatio), queryCdpsByRatioHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/cdp/cdps"), queryCdpsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/cdp/cdps/collateralType/{%s}", types.RestCollateralType), queryCdpsByCollateralTypeHandlerFn(cliCtx)).Methods("GET") // legacy + r.HandleFunc(fmt.Sprintf("/cdp/cdps/ratio/{%s}/{%s}", types.RestCollateralType, types.RestRatio), queryCdpsByRatioHandlerFn(cliCtx)).Methods("GET") // legacy r.HandleFunc(fmt.Sprintf("/cdp/cdps/cdp/deposits/{%s}/{%s}", types.RestOwner, types.RestCollateralType), queryCdpDepositsHandlerFn(cliCtx)).Methods("GET") } @@ -58,7 +62,7 @@ func queryCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } -func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func queryCdpsByCollateralTypeHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) if !ok { @@ -68,7 +72,7 @@ func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { vars := mux.Vars(r) collateralType := vars[types.RestCollateralType] - params := types.NewQueryCdpsParams(collateralType) + params := types.NewQueryCdpsByCollateralTypeParams(collateralType) bz, err := cliCtx.Codec.MarshalJSON(params) if err != nil { @@ -76,7 +80,7 @@ func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), bz) + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdpsByCollateralType), bz) if err != nil { rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return @@ -193,3 +197,91 @@ func getAccountsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { rest.PostProcessResponse(w, cliCtx, res) } } + +func getSavingsRateDistributedHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetSavingsRateDistributed), nil) + cliCtx = cliCtx.WithHeight(height) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + var cdpCollateralType string + var cdpOwner sdk.AccAddress + var cdpID uint64 + var cdpRatio sdk.Dec + + if x := r.URL.Query().Get(RestCollateralType); len(x) != 0 { + cdpCollateralType = strings.TrimSpace(x) + } + + if x := r.URL.Query().Get(RestOwner); len(x) != 0 { + cdpOwnerStr := strings.ToLower(strings.TrimSpace(x)) + cdpOwner, err = sdk.AccAddressFromBech32(cdpOwnerStr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from cdp owner %s", cdpOwnerStr)) + return + } + } + + if x := r.URL.Query().Get(RestID); len(x) != 0 { + cdpID, err = strconv.ParseUint(strings.TrimSpace(x), 10, 64) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + } + + if x := r.URL.Query().Get(RestRatio); len(x) != 0 { + cdpRatio, err = sdk.NewDecFromStr(strings.TrimSpace(x)) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + } else { + // Set to sdk.Dec(0) so that if not specified in params it doesn't panic when unmarshaled + cdpRatio = sdk.ZeroDec() + } + + params := types.NewQueryCdpsParams(page, limit, cdpCollateralType, cdpOwner, cdpID, cdpRatio) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetCdps) + res, height, err := cliCtx.QueryWithData(route, bz) + cliCtx = cliCtx.WithHeight(height) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/cdp/client/rest/rest.go b/x/cdp/client/rest/rest.go index 49c170d9..068c4cdb 100644 --- a/x/cdp/client/rest/rest.go +++ b/x/cdp/client/rest/rest.go @@ -8,6 +8,15 @@ import ( "github.com/cosmos/cosmos-sdk/types/rest" ) +// REST Variable names +// nolint +const ( + RestOwner = "owner" + RestCollateralType = "collateral-type" + RestID = "id" + RestRatio = "ratio" +) + // RegisterRoutes - Central function to define routes that get registered by the main application func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { registerQueryRoutes(cliCtx, r) diff --git a/x/cdp/client/rest/tx.go b/x/cdp/client/rest/tx.go index dff087bf..154d9b39 100644 --- a/x/cdp/client/rest/tx.go +++ b/x/cdp/client/rest/tx.go @@ -20,7 +20,6 @@ func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc("/cdp/{owner}/{collateralType}/withdraw", postWithdrawHandlerFn(cliCtx)).Methods("POST") r.HandleFunc("/cdp/{owner}/{collateralType}/draw", postDrawHandlerFn(cliCtx)).Methods("POST") r.HandleFunc("/cdp/{owner}/{collateralType}/repay", postRepayHandlerFn(cliCtx)).Methods("POST") - } func postCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index 3f4973f4..95936958 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -88,6 +88,8 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk types.PricefeedKeeper, sk types.S for _, d := range gs.Deposits { k.SetDeposit(ctx, d) } + + k.SetSavingsRateDistributed(ctx, gs.SavingsRateDistributed) } // ExportGenesis export genesis state for cdp module @@ -108,11 +110,12 @@ func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { cdpID := k.GetNextCdpID(ctx) debtDenom := k.GetDebtDenom(ctx) govDenom := k.GetGovDenom(ctx) + savingsRateDist := k.GetSavingsRateDistributed(ctx) previousDistributionTime, found := k.GetPreviousSavingsDistribution(ctx) if !found { previousDistributionTime = DefaultPreviousDistributionTime } - return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousDistributionTime) + return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousDistributionTime, savingsRateDist) } diff --git a/x/cdp/genesis_test.go b/x/cdp/genesis_test.go index 849583eb..947867eb 100644 --- a/x/cdp/genesis_test.go +++ b/x/cdp/genesis_test.go @@ -22,13 +22,14 @@ type GenesisTestSuite struct { func (suite *GenesisTestSuite) TestInvalidGenState() { type args struct { - params cdp.Params - cdps cdp.CDPs - deposits cdp.Deposits - startingID uint64 - debtDenom string - govDenom string - prevDistTime time.Time + params cdp.Params + cdps cdp.CDPs + deposits cdp.Deposits + startingID uint64 + debtDenom string + govDenom string + prevDistTime time.Time + savingsRateDist sdk.Int } type errArgs struct { expectPass bool @@ -47,12 +48,13 @@ func (suite *GenesisTestSuite) TestInvalidGenState() { { name: "empty debt denom", args: args{ - params: cdp.DefaultParams(), - cdps: cdp.CDPs{}, - deposits: cdp.Deposits{}, - debtDenom: "", - govDenom: cdp.DefaultGovDenom, - prevDistTime: cdp.DefaultPreviousDistributionTime, + params: cdp.DefaultParams(), + cdps: cdp.CDPs{}, + deposits: cdp.Deposits{}, + debtDenom: "", + govDenom: cdp.DefaultGovDenom, + prevDistTime: cdp.DefaultPreviousDistributionTime, + savingsRateDist: cdp.DefaultSavingsRateDistributed, }, errArgs: errArgs{ expectPass: false, @@ -62,12 +64,13 @@ func (suite *GenesisTestSuite) TestInvalidGenState() { { name: "empty gov denom", args: args{ - params: cdp.DefaultParams(), - cdps: cdp.CDPs{}, - deposits: cdp.Deposits{}, - debtDenom: cdp.DefaultDebtDenom, - govDenom: "", - prevDistTime: cdp.DefaultPreviousDistributionTime, + params: cdp.DefaultParams(), + cdps: cdp.CDPs{}, + deposits: cdp.Deposits{}, + debtDenom: cdp.DefaultDebtDenom, + govDenom: "", + prevDistTime: cdp.DefaultPreviousDistributionTime, + savingsRateDist: cdp.DefaultSavingsRateDistributed, }, errArgs: errArgs{ expectPass: false, @@ -77,27 +80,46 @@ func (suite *GenesisTestSuite) TestInvalidGenState() { { name: "empty distribution time", args: args{ - params: cdp.DefaultParams(), - cdps: cdp.CDPs{}, - deposits: cdp.Deposits{}, - debtDenom: cdp.DefaultDebtDenom, - govDenom: cdp.DefaultGovDenom, - prevDistTime: time.Time{}, + params: cdp.DefaultParams(), + cdps: cdp.CDPs{}, + deposits: cdp.Deposits{}, + debtDenom: cdp.DefaultDebtDenom, + govDenom: cdp.DefaultGovDenom, + prevDistTime: time.Time{}, + savingsRateDist: cdp.DefaultSavingsRateDistributed, }, errArgs: errArgs{ expectPass: false, contains: "previous distribution time not set", }, }, + { + name: "negative savings rate distributed", + args: args{ + params: cdp.DefaultParams(), + cdps: cdp.CDPs{}, + deposits: cdp.Deposits{}, + debtDenom: cdp.DefaultDebtDenom, + govDenom: cdp.DefaultGovDenom, + prevDistTime: cdp.DefaultPreviousDistributionTime, + savingsRateDist: sdk.NewInt(-100), + }, + errArgs: errArgs{ + expectPass: false, + contains: "savings rate distributed should not be negative", + }, + }, } for _, tc := range testCases { suite.Run(tc.name, func() { - gs := cdp.NewGenesisState(tc.args.params, tc.args.cdps, tc.args.deposits, tc.args.startingID, tc.args.debtDenom, tc.args.govDenom, tc.args.prevDistTime) + gs := cdp.NewGenesisState(tc.args.params, tc.args.cdps, tc.args.deposits, tc.args.startingID, + tc.args.debtDenom, tc.args.govDenom, tc.args.prevDistTime, tc.args.savingsRateDist) err := gs.Validate() if tc.errArgs.expectPass { suite.Require().NoError(err) } else { suite.Require().Error(err) + suite.T().Log(err) suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) } }) diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go index 51587794..589e15e8 100644 --- a/x/cdp/keeper/keeper.go +++ b/x/cdp/keeper/keeper.go @@ -110,3 +110,23 @@ func (k Keeper) IterateCdpsByCollateralRatio(ctx sdk.Context, collateralType str } } + +// SetSavingsRateDistributed sets the SavingsRateDistributed in the store +func (k Keeper) SetSavingsRateDistributed(ctx sdk.Context, totalDistributed sdk.Int) { + store := prefix.NewStore(ctx.KVStore(k.key), types.SavingsRateDistributedKey) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(totalDistributed) + store.Set([]byte{}, bz) +} + +// GetSavingsRateDistributed gets the SavingsRateDistributed from the store +func (k Keeper) GetSavingsRateDistributed(ctx sdk.Context) sdk.Int { + savingsRateDistributed := sdk.ZeroInt() + store := prefix.NewStore(ctx.KVStore(k.key), types.SavingsRateDistributedKey) + bz := store.Get([]byte{}) + if bz == nil { + return savingsRateDistributed + } + + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &savingsRateDistributed) + return savingsRateDistributed +} diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go new file mode 100644 index 00000000..551866be --- /dev/null +++ b/x/cdp/keeper/keeper_test.go @@ -0,0 +1,48 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" +) + +type KeeperTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *KeeperTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + suite.ResetChain() + return +} + +func (suite *KeeperTestSuite) ResetChain() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + keeper := tApp.GetCDPKeeper() + + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper +} + +func (suite *KeeperTestSuite) TestGetSetSavingsRateDistributed() { + suite.ResetChain() + + // Set savings rate distributed value + savingsRateDist := sdk.NewInt(555000555000) + suite.keeper.SetSavingsRateDistributed(suite.ctx, savingsRateDist) + + // Check store's savings rate distributed value + s := suite.keeper.GetSavingsRateDistributed(suite.ctx) + suite.Equal(savingsRateDist, s) +} diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index 8af78ebf..1ca718ec 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -31,6 +31,16 @@ func (k Keeper) GetCollateral(ctx sdk.Context, collateralType string) (types.Col return types.CollateralParam{}, false } +// GetCollateralTypes returns an array of collateral types +func (k Keeper) GetCollateralTypes(ctx sdk.Context) []string { + params := k.GetParams(ctx) + var denoms []string + for _, cp := range params.CollateralParams { + denoms = append(denoms, cp.Type) + } + return denoms +} + // GetDebtParam returns the debt param with matching denom func (k Keeper) GetDebtParam(ctx sdk.Context, denom string) (types.DebtParam, bool) { dp := k.GetParams(ctx).DebtParam diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go index df9baf83..edb870c2 100644 --- a/x/cdp/keeper/querier.go +++ b/x/cdp/keeper/querier.go @@ -3,6 +3,7 @@ package keeper import ( abci "github.com/tendermint/tendermint/abci/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -18,15 +19,19 @@ func NewQuerier(keeper Keeper) sdk.Querier { case types.QueryGetCdp: return queryGetCdp(ctx, req, keeper) case types.QueryGetCdps: + return queryGetCdps(ctx, req, keeper) + case types.QueryGetCdpDeposits: + return queryGetDeposits(ctx, req, keeper) + case types.QueryGetCdpsByCollateralType: // legacy, maintained for REST API return queryGetCdpsByCollateralType(ctx, req, keeper) - case types.QueryGetCdpsByCollateralization: + case types.QueryGetCdpsByCollateralization: // legacy, maintained for REST API return queryGetCdpsByRatio(ctx, req, keeper) case types.QueryGetParams: return queryGetParams(ctx, req, keeper) - case types.QueryGetCdpDeposits: - return queryGetDeposits(ctx, req, keeper) case types.QueryGetAccounts: return queryGetAccounts(ctx, req, keeper) + case types.QueryGetSavingsRateDistributed: + return queryGetSavingsRateDistributed(ctx, req, keeper) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint %s", types.ModuleName, path[0]) } @@ -43,7 +48,7 @@ func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, _, valid := keeper.GetCollateralTypePrefix(ctx, requestParams.CollateralType) if !valid { - return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralType) + return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralType) } cdp, found := keeper.GetCdpByOwnerAndCollateralType(ctx, requestParams.Owner, requestParams.CollateralType) @@ -71,7 +76,7 @@ func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([] _, valid := keeper.GetCollateralTypePrefix(ctx, requestParams.CollateralType) if !valid { - return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralType) + return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralType) } cdp, found := keeper.GetCdpByOwnerAndCollateralType(ctx, requestParams.Owner, requestParams.CollateralType) @@ -98,7 +103,7 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) } _, valid := keeper.GetCollateralTypePrefix(ctx, requestParams.CollateralType) if !valid { - return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralType) + return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralType) } ratio, err := keeper.CalculateCollateralizationRatioFromAbsoluteRatio(ctx, requestParams.CollateralType, requestParams.Ratio, "liquidation") @@ -120,16 +125,16 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) return bz, nil } -// query all cdps with matching collateral denom +// query all cdps with matching collateral type func queryGetCdpsByCollateralType(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) { - var requestParams types.QueryCdpsParams + var requestParams types.QueryCdpsByCollateralTypeParams err := types.ModuleCdc.UnmarshalJSON(req.Data, &requestParams) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } _, valid := keeper.GetCollateralTypePrefix(ctx, requestParams.CollateralType) if !valid { - return nil, sdkerrors.Wrap(types.ErrCollateralNotSupported, requestParams.CollateralType) + return nil, sdkerrors.Wrap(types.ErrInvalidCollateral, requestParams.CollateralType) } cdps := keeper.GetAllCdpsByCollateralType(ctx, requestParams.CollateralType) @@ -178,3 +183,175 @@ func queryGetAccounts(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([] } return bz, nil } + +// query get savings rate distributed in the cdp store +func queryGetSavingsRateDistributed(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) { + // Get savings rate distributed + savingsRateDist := keeper.GetSavingsRateDistributed(ctx) + + // Encode results + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, savingsRateDist) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +// query cdps in store and filter by request params +func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) { + var params types.QueryCdpsParams + err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + // Filter CDPs + filteredCDPs := FilterCDPs(ctx, keeper, params) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, filteredCDPs) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +// FilterCDPs queries the store for all CDPs that match query params +func FilterCDPs(ctx sdk.Context, k Keeper, params types.QueryCdpsParams) types.AugmentedCDPs { + var matchCollateralType, matchOwner, matchID, matchRatio types.CDPs + + // match cdp owner (if supplied) + if len(params.Owner) > 0 { + denoms := k.GetCollateralTypes(ctx) + for _, denom := range denoms { + cdp, found := k.GetCdpByOwnerAndCollateralType(ctx, params.Owner, denom) + if found { + matchOwner = append(matchOwner, cdp) + } + } + } + + // match cdp collateral denom (if supplied) + if len(params.CollateralType) > 0 { + // if owner is specified only iterate over already matched cdps for efficiency + if len(params.Owner) > 0 { + for _, cdp := range matchOwner { + if cdp.Type == params.CollateralType { + matchCollateralType = append(matchCollateralType, cdp) + } + } + } else { + matchCollateralType = k.GetAllCdpsByCollateralType(ctx, params.CollateralType) + } + } + + // match cdp ID (if supplied) + if params.ID != 0 { + denoms := k.GetCollateralTypes(ctx) + for _, denom := range denoms { + cdp, found := k.GetCDP(ctx, denom, params.ID) + if found { + matchID = append(matchID, cdp) + } + } + } + + // match cdp ratio (if supplied) + if params.Ratio.GT(sdk.ZeroDec()) { + denoms := k.GetCollateralTypes(ctx) + for _, denom := range denoms { + ratio, err := k.CalculateCollateralizationRatioFromAbsoluteRatio(ctx, denom, params.Ratio, "liquidation") + if err != nil { + continue + } + cdpsUnderRatio := k.GetAllCdpsByCollateralTypeAndRatio(ctx, denom, ratio) + matchRatio = append(matchRatio, cdpsUnderRatio...) + } + } + + var commonCDPs types.CDPs + // If no params specified, fetch all CDPs + if len(params.CollateralType) == 0 && len(params.Owner) == 0 && params.ID == 0 && params.Ratio.Equal(sdk.ZeroDec()) { + commonCDPs = k.GetAllCdps(ctx) + } + + // Find the intersection of any matched CDPs + if len(params.CollateralType) > 0 { + if len(matchCollateralType) > 0 { + commonCDPs = matchCollateralType + } else { + return types.AugmentedCDPs{} + } + } + + if len(params.Owner) > 0 { + if len(matchCollateralType) > 0 { + if len(commonCDPs) > 0 { + commonCDPs = FindIntersection(commonCDPs, matchOwner) + } else { + commonCDPs = matchOwner + } + } else { + commonCDPs = matchOwner + } + } + if params.ID != 0 { + if len(matchID) > 0 { + if len(commonCDPs) > 0 { + commonCDPs = FindIntersection(commonCDPs, matchID) + } else { + commonCDPs = matchID + } + } else { + return types.AugmentedCDPs{} + } + } + if params.Ratio.GT(sdk.ZeroDec()) { + if len(matchRatio) > 0 { + if len(commonCDPs) > 0 { + commonCDPs = FindIntersection(commonCDPs, matchRatio) + } else { + commonCDPs = matchRatio + } + } else { + return types.AugmentedCDPs{} + } + } + // Load augmented CDPs + var augmentedCDPs types.AugmentedCDPs + for _, cdp := range commonCDPs { + augmentedCDP := k.LoadAugmentedCDP(ctx, cdp) + augmentedCDPs = append(augmentedCDPs, augmentedCDP) + } + + // Apply page and limit params + var paginatedCDPs types.AugmentedCDPs + start, end := client.Paginate(len(augmentedCDPs), params.Page, params.Limit, 100) + if start < 0 || end < 0 { + paginatedCDPs = types.AugmentedCDPs{} + } else { + paginatedCDPs = augmentedCDPs[start:end] + } + + return paginatedCDPs +} + +// FindIntersection finds the intersection of two CDP arrays in linear time complexity O(n + n) +func FindIntersection(x types.CDPs, y types.CDPs) types.CDPs { + cdpSet := make(types.CDPs, 0) + cdpMap := make(map[uint64]bool) + + for i := 0; i < len(x); i++ { + cdp := x[i] + cdpMap[cdp.ID] = true + } + + for i := 0; i < len(y); i++ { + cdp := y[i] + if _, found := cdpMap[cdp.ID]; found { + cdpSet = append(cdpSet, cdp) + } + } + + return cdpSet +} diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go index 3032e8c6..f52c6c74 100644 --- a/x/cdp/keeper/querier_test.go +++ b/x/cdp/keeper/querier_test.go @@ -116,7 +116,7 @@ func (suite *QuerierTestSuite) TestQueryCdp() { ctx := suite.ctx.WithIsCheckTx(false) query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, suite.cdps[0].Collateral.Denom+"-a")), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, suite.cdps[0].Type)), } bz, err := suite.querier(ctx, []string{types.QueryGetCdp}, query) suite.Nil(err) @@ -156,10 +156,10 @@ func (suite *QuerierTestSuite) TestQueryCdp() { func (suite *QuerierTestSuite) TestQueryCdpsByCollateralType() { ctx := suite.ctx.WithIsCheckTx(false) query := abci.RequestQuery{ - Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams(suite.cdps[0].Collateral.Denom + "-a")), + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralType}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByCollateralTypeParams(suite.cdps[0].Type)), } - bz, err := suite.querier(ctx, []string{types.QueryGetCdps}, query) + bz, err := suite.querier(ctx, []string{types.QueryGetCdpsByCollateralType}, query) suite.Nil(err) suite.NotNil(bz) @@ -168,10 +168,10 @@ func (suite *QuerierTestSuite) TestQueryCdpsByCollateralType() { suite.Equal(50, len(c)) query = abci.RequestQuery{ - Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams("lol-a")), + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralType}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByCollateralTypeParams("lol-a")), } - _, err = suite.querier(ctx, []string{types.QueryGetCdps}, query) + _, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralType}, query) suite.Error(err) } @@ -265,7 +265,7 @@ func (suite *QuerierTestSuite) TestQueryDeposits() { ctx := suite.ctx.WithIsCheckTx(false) query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpDeposits}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpDeposits(suite.cdps[0].Owner, suite.cdps[0].Collateral.Denom+"-a")), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpDeposits(suite.cdps[0].Owner, suite.cdps[0].Type)), } bz, err := suite.querier(ctx, []string{types.QueryGetCdpDeposits}, query) @@ -303,6 +303,89 @@ func (suite *QuerierTestSuite) TestQueryAccounts() { suite.Require().True(findByName("savings")) } +func (suite *QuerierTestSuite) TestQuerySavingsRateDistributed() { + ctx := suite.ctx.WithIsCheckTx(false) + bz, err := suite.querier(ctx, []string{types.QueryGetSavingsRateDistributed}, abci.RequestQuery{}) + suite.Nil(err) + suite.NotNil(bz) + + var distAmount sdk.Int + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &distAmount)) + suite.True(sdk.ZeroInt().Equal(distAmount)) +} + +func (suite *QuerierTestSuite) TestFindIntersection() { + a := types.CDPs{suite.cdps[0], suite.cdps[1], suite.cdps[2], suite.cdps[3], suite.cdps[4]} + b := types.CDPs{suite.cdps[3], suite.cdps[4], suite.cdps[5], suite.cdps[6], suite.cdps[7]} + expectedIntersection1 := types.CDPs{suite.cdps[3], suite.cdps[4]} + + intersection1 := keeper.FindIntersection(a, b) + suite.Require().Equal(intersection1, expectedIntersection1) + + c := types.CDPs{suite.cdps[0], suite.cdps[1], suite.cdps[2], suite.cdps[3], suite.cdps[4]} + d := types.CDPs{suite.cdps[5], suite.cdps[6], suite.cdps[7], suite.cdps[8], suite.cdps[9]} + expectedIntersection2 := types.CDPs{} + + intersection2 := keeper.FindIntersection(c, d) + suite.Require().Equal(intersection2, expectedIntersection2) + + e := types.CDPs{suite.cdps[0]} + f := types.CDPs{} + expectedIntersection3 := types.CDPs{} + + intersection3 := keeper.FindIntersection(e, f) + suite.Require().Equal(intersection3, expectedIntersection3) +} + +func (suite *QuerierTestSuite) TestFilterCDPs() { + paramsType := types.NewQueryCdpsParams(1, 100, "btc-a", sdk.AccAddress{}, 0, sdk.ZeroDec()) + filteredCDPs1 := keeper.FilterCDPs(suite.ctx, suite.keeper, paramsType) + suite.Require().Equal(len(filteredCDPs1), 50) + + paramsOwner := types.NewQueryCdpsParams(1, 100, "", suite.cdps[10].Owner, 0, sdk.ZeroDec()) + filteredCDPs2 := keeper.FilterCDPs(suite.ctx, suite.keeper, paramsOwner) + suite.Require().Equal(len(filteredCDPs2), 1) + suite.Require().Equal(filteredCDPs2[0].Owner, suite.cdps[10].Owner) + + paramsID := types.NewQueryCdpsParams(1, 100, "", sdk.AccAddress{}, 68, sdk.ZeroDec()) + filteredCDPs3 := keeper.FilterCDPs(suite.ctx, suite.keeper, paramsID) + suite.Require().Equal(len(filteredCDPs3), 1) + suite.Require().Equal(filteredCDPs3[0].ID, suite.cdps[68-1].ID) + + ratioCountBtc := 0 + btcRatio := d("2500") + for _, cdp := range suite.cdps { + if cdp.Type == "btc-a" { + absoluteRatio := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Type, cdp.Principal) + collateralizationRatio, _ := suite.keeper.CalculateCollateralizationRatioFromAbsoluteRatio(suite.ctx, cdp.Type, absoluteRatio, "liquidation") + if collateralizationRatio.LT(btcRatio) { + ratioCountBtc += 1 + } + } + } + paramsTypeAndRatio := types.NewQueryCdpsParams(1, 100, "btc-a", sdk.AccAddress{}, 0, sdk.NewDec(2500)) + filteredCDPs4 := keeper.FilterCDPs(suite.ctx, suite.keeper, paramsTypeAndRatio) + suite.Require().Equal(len(filteredCDPs4), ratioCountBtc) +} + +func (suite *QuerierTestSuite) TestQueryCdps() { + ctx := suite.ctx.WithIsCheckTx(false) + params := types.NewQueryCdpsParams(1, 100, "btc-a", sdk.AccAddress{}, 0, sdk.ZeroDec()) + + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(params), + } + + bz, err := suite.querier(ctx, []string{types.QueryGetCdps}, query) + suite.Nil(err) + suite.NotNil(bz) + + output := types.AugmentedCDPs{} + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &output)) + suite.Equal(50, len(output)) +} + func TestQuerierTestSuite(t *testing.T) { suite.Run(t, new(QuerierTestSuite)) } diff --git a/x/cdp/keeper/savings.go b/x/cdp/keeper/savings.go index 0a443188..779507f0 100644 --- a/x/cdp/keeper/savings.go +++ b/x/cdp/keeper/savings.go @@ -58,6 +58,11 @@ func (k Keeper) DistributeSavingsRate(ctx sdk.Context, debtDenom string) error { return false } + // update total savings rate distributed by surplus to distribute + previousSavingsDistributed := k.GetSavingsRateDistributed(ctx) + newTotalDistributed := previousSavingsDistributed.Add(interest) + k.SetSavingsRateDistributed(ctx, newTotalDistributed) + interestCoins := sdk.NewCoins(sdk.NewCoin(debtDenom, interest)) err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.SavingsRateMacc, acc.GetAddress(), interestCoins) if err != nil { diff --git a/x/cdp/keeper/savings_test.go b/x/cdp/keeper/savings_test.go index ed3db27f..957fb994 100644 --- a/x/cdp/keeper/savings_test.go +++ b/x/cdp/keeper/savings_test.go @@ -18,15 +18,17 @@ import ( type SavingsTestSuite struct { suite.Suite - keeper keeper.Keeper - app app.TestApp - ctx sdk.Context - addrs []sdk.AccAddress + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress + amountToDistribute int64 } func (suite *SavingsTestSuite) SetupTest() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + _, addrs := app.GeneratePrivKeyAddressPairs(3) authGS := app.NewAuthGenState( addrs, @@ -34,25 +36,33 @@ func (suite *SavingsTestSuite) SetupTest() { cs(c("usdx", 100000)), cs(c("usdx", 50000)), cs(c("usdx", 50000)), }, ) + tApp.InitializeFromGenesisStates( authGS, NewPricefeedGenStateMulti(), NewCDPGenStateMulti(), ) + sk := tApp.GetSupplyKeeper() macc := sk.GetModuleAccount(ctx, types.SavingsRateMacc) - err := sk.MintCoins(ctx, macc.GetName(), cs(c("usdx", 10000))) + distAmount := int64(10000) + err := sk.MintCoins(ctx, macc.GetName(), cs(c("usdx", distAmount))) suite.NoError(err) + keeper := tApp.GetCDPKeeper() suite.app = tApp suite.keeper = keeper suite.ctx = ctx suite.addrs = addrs + suite.amountToDistribute = distAmount } func (suite *SavingsTestSuite) TestApplySavingsRate() { + preSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx) + err := suite.keeper.DistributeSavingsRate(suite.ctx, "usdx") suite.NoError(err) + ak := suite.app.GetAccountKeeper() acc0 := ak.GetAccount(suite.ctx, suite.addrs[0]) suite.Equal(cs(c("usdx", 105000)), acc0.GetCoins()) @@ -60,9 +70,14 @@ func (suite *SavingsTestSuite) TestApplySavingsRate() { suite.Equal(cs(c("usdx", 52500)), acc1.GetCoins()) acc2 := ak.GetAccount(suite.ctx, suite.addrs[2]) suite.Equal(cs(c("usdx", 52500)), acc2.GetCoins()) + sk := suite.app.GetSupplyKeeper() macc := sk.GetModuleAccount(suite.ctx, types.SavingsRateMacc) suite.True(macc.GetCoins().AmountOf("usdx").IsZero()) + + expectedPostSavingsRateDistAmount := preSavingsRateDistAmount.Add(sdk.NewInt(suite.amountToDistribute)) + postSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx) + suite.True(expectedPostSavingsRateDistAmount.Equal(postSavingsRateDistAmount)) } func (suite *SavingsTestSuite) TestGetSetPreviousDistributionTime() { @@ -76,7 +91,21 @@ func (suite *SavingsTestSuite) TestGetSetPreviousDistributionTime() { pdt, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx) suite.True(f) suite.Equal(now, pdt) +} +func (suite *SavingsTestSuite) TestGetSetSavingsRateDistributed() { + // Savings rate dist set to 0 when the default genesis is used + preSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx) + suite.True(preSavingsRateDistAmount.Equal(types.DefaultSavingsRateDistributed)) + + // Adding new dist amount to existing dist so default genesis value can be updated in the future + amountToDistribute := sdk.NewInt(9876543210) + newTotalDistributed := preSavingsRateDistAmount.Add(amountToDistribute) + + suite.NotPanics(func() { suite.keeper.SetSavingsRateDistributed(suite.ctx, newTotalDistributed) }) + + postSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx) + suite.Equal(newTotalDistributed, postSavingsRateDistAmount) } func TestSavingsTestSuite(t *testing.T) { diff --git a/x/cdp/types/cdp.go b/x/cdp/types/cdp.go index 5cbecba8..8c2582df 100644 --- a/x/cdp/types/cdp.go +++ b/x/cdp/types/cdp.go @@ -121,6 +121,7 @@ func NewAugmentedCDP(cdp CDP, collateralValue sdk.Coin, collateralizationRatio s CDP: CDP{ ID: cdp.ID, Owner: cdp.Owner, + Type: cdp.Type, Collateral: cdp.Collateral, Principal: cdp.Principal, AccumulatedFees: cdp.AccumulatedFees, @@ -146,7 +147,7 @@ func (augCDP AugmentedCDP) String() string { Collateralization ratio: %s`, augCDP.Owner, augCDP.ID, - augCDP.Collateral.Denom, + augCDP.Type, augCDP.Collateral, augCDP.CollateralValue, augCDP.Principal, diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 64c56f06..edd4cb9c 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -17,10 +17,12 @@ type GenesisState struct { DebtDenom string `json:"debt_denom" yaml:"debt_denom"` GovDenom string `json:"gov_denom" yaml:"gov_denom"` PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"` + SavingsRateDistributed sdk.Int `json:"savings_rate_distributed" yaml:"savings_rate_distributed"` } // NewGenesisState returns a new genesis state -func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, debtDenom, govDenom string, previousDistTime time.Time) GenesisState { +func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, + debtDenom, govDenom string, previousDistTime time.Time, savingsRateDist sdk.Int) GenesisState { return GenesisState{ Params: params, CDPs: cdps, @@ -29,6 +31,7 @@ func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID DebtDenom: debtDenom, GovDenom: govDenom, PreviousDistributionTime: previousDistTime, + SavingsRateDistributed: savingsRateDist, } } @@ -42,6 +45,7 @@ func DefaultGenesisState() GenesisState { DefaultDebtDenom, DefaultGovDenom, DefaultPreviousDistributionTime, + DefaultSavingsRateDistributed, ) } @@ -65,6 +69,10 @@ func (gs GenesisState) Validate() error { return fmt.Errorf("previous distribution time not set") } + if err := validateSavingsRateDistributed(gs.SavingsRateDistributed); err != nil { + return err + } + if err := sdk.ValidateDenom(gs.DebtDenom); err != nil { return fmt.Errorf(fmt.Sprintf("debt denom invalid: %v", err)) } @@ -76,6 +84,19 @@ func (gs GenesisState) Validate() error { return nil } +func validateSavingsRateDistributed(i interface{}) error { + savingsRateDist, ok := i.(sdk.Int) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if savingsRateDist.IsNegative() { + return fmt.Errorf("savings rate distributed should not be negative: %s", savingsRateDist) + } + + return nil +} + // Equal checks whether two gov GenesisState structs are equivalent func (gs GenesisState) Equal(gs2 GenesisState) bool { b1 := ModuleCdc.MustMarshalBinaryBare(gs) diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go index 53570c66..2fe2dd82 100644 --- a/x/cdp/types/keys.go +++ b/x/cdp/types/keys.go @@ -47,6 +47,7 @@ var sep = []byte(":") // - 0x07:feeRate // - 0x08:previousDistributionTime // - 0x09:downTime +// - 0x10:totalDistributed // KVStore key prefixes var ( @@ -60,6 +61,7 @@ var ( PrincipalKeyPrefix = []byte{0x07} PreviousDistributionTimeKey = []byte{0x08} PricefeedStatusKeyPrefix = []byte{0x09} + SavingsRateDistributedKey = []byte{0x10} ) // GetCdpIDBytes returns the byte representation of the cdpID diff --git a/x/cdp/types/msg.go b/x/cdp/types/msg.go index 03abe6a1..ba2fe6b2 100644 --- a/x/cdp/types/msg.go +++ b/x/cdp/types/msg.go @@ -76,7 +76,8 @@ func (msg MsgCreateCDP) String() string { Sender: %s Collateral: %s Principal: %s -`, msg.Sender, msg.Collateral, msg.Principal) + Collateral Type: %s +`, msg.Sender, msg.Collateral, msg.Principal, msg.CollateralType) } // MsgDeposit deposit collateral to an existing cdp. diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index a355ba8c..68348d38 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -14,19 +14,20 @@ import ( // Parameter keys var ( - KeyGlobalDebtLimit = []byte("GlobalDebtLimit") - KeyCollateralParams = []byte("CollateralParams") - KeyDebtParam = []byte("DebtParam") - KeyDistributionFrequency = []byte("DistributionFrequency") - KeyCircuitBreaker = []byte("CircuitBreaker") - KeyDebtThreshold = []byte("DebtThreshold") - KeyDebtLot = []byte("DebtLot") - KeySurplusThreshold = []byte("SurplusThreshold") - KeySurplusLot = []byte("SurplusLot") - DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt()) - DefaultCircuitBreaker = false - DefaultCollateralParams = CollateralParams{} - DefaultDebtParam = DebtParam{ + KeyGlobalDebtLimit = []byte("GlobalDebtLimit") + KeyCollateralParams = []byte("CollateralParams") + KeyDebtParam = []byte("DebtParam") + KeyDistributionFrequency = []byte("DistributionFrequency") + KeyCircuitBreaker = []byte("CircuitBreaker") + KeyDebtThreshold = []byte("DebtThreshold") + KeyDebtLot = []byte("DebtLot") + KeySurplusThreshold = []byte("SurplusThreshold") + KeySurplusLot = []byte("SurplusLot") + KeySavingsRateDistributed = []byte("SavingsRateDistributed") + DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt()) + DefaultCircuitBreaker = false + DefaultCollateralParams = CollateralParams{} + DefaultDebtParam = DebtParam{ Denom: "usdx", ReferenceAsset: "usd", ConversionFactor: sdk.NewInt(6), @@ -43,6 +44,7 @@ var ( DefaultDebtLot = sdk.NewInt(10000000000) DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0)) DefaultSavingsDistributionFrequency = time.Hour * 12 + DefaultSavingsRateDistributed = sdk.NewInt(0) minCollateralPrefix = 0 maxCollateralPrefix = 255 stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR diff --git a/x/cdp/types/querier.go b/x/cdp/types/querier.go index f6dd25a4..4785d84f 100644 --- a/x/cdp/types/querier.go +++ b/x/cdp/types/querier.go @@ -7,28 +7,18 @@ import ( // Querier routes for the cdp module const ( QueryGetCdp = "cdp" - QueryGetCdpDeposits = "deposits" QueryGetCdps = "cdps" - QueryGetCdpsByCollateralization = "ratio" + QueryGetCdpDeposits = "deposits" + QueryGetCdpsByCollateralization = "ratio" // legacy query, maintained for REST API + QueryGetCdpsByCollateralType = "collateralType" // legacy query, maintained for REST API QueryGetParams = "params" QueryGetAccounts = "accounts" + QueryGetSavingsRateDistributed = "savings-rate-dist" RestOwner = "owner" RestCollateralType = "collateral-type" RestRatio = "ratio" ) -// QueryCdpsParams params for query /cdp/cdps -type QueryCdpsParams struct { - CollateralType string // get CDPs with this collateral type -} - -// NewQueryCdpsParams returns QueryCdpsParams -func NewQueryCdpsParams(collateralType string) QueryCdpsParams { - return QueryCdpsParams{ - CollateralType: collateralType, - } -} - // QueryCdpParams params for query /cdp/cdp type QueryCdpParams struct { CollateralType string // get CDPs with this collateral type @@ -43,6 +33,28 @@ func NewQueryCdpParams(owner sdk.AccAddress, collateralType string) QueryCdpPara } } +// QueryCdpsParams is the params for a filtered CDP query +type QueryCdpsParams struct { + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + ID uint64 `json:"id" yaml:"id"` + Ratio sdk.Dec `json:"ratio" yaml:"ratio"` +} + +// NewQueryCdpsParams creates a new QueryCdpsParams +func NewQueryCdpsParams(page, limit int, collateralType string, owner sdk.AccAddress, id uint64, ratio sdk.Dec) QueryCdpsParams { + return QueryCdpsParams{ + Page: page, + Limit: limit, + CollateralType: collateralType, + Owner: owner, + ID: id, + Ratio: ratio, + } +} + // QueryCdpDeposits params for query /cdp/deposits type QueryCdpDeposits struct { CollateralType string // get CDPs with this collateral type @@ -57,6 +69,18 @@ func NewQueryCdpDeposits(owner sdk.AccAddress, collateralType string) QueryCdpDe } } +// QueryCdpsByCollateralTypeParams params for query /cdp/cdps/{denom} +type QueryCdpsByCollateralTypeParams struct { + CollateralType string // get CDPs with this collateral type +} + +// NewQueryCdpsByCollateralTypeParams returns QueryCdpsByCollateralTypeParams +func NewQueryCdpsByCollateralTypeParams(collateralType string) QueryCdpsByCollateralTypeParams { + return QueryCdpsByCollateralTypeParams{ + CollateralType: collateralType, + } +} + // QueryCdpsByRatioParams params for query /cdp/cdps/ratio type QueryCdpsByRatioParams struct { CollateralType string