From 23e054e40221b2160f5a3bcd0b41c16c1a580f60 Mon Sep 17 00:00:00 2001 From: drklee3 Date: Thu, 6 Apr 2023 15:51:34 -0700 Subject: [PATCH] feat: re-add validator-vesting legacy rest endpoints (#1542) * Re-add validator-vesting legacy rest endpoints * Add changelog entry * Re-add /vesting swagger config --- CHANGELOG.md | 7 +- app/app.go | 4 + client/docs/config.json | 6 + client/docs/legacy-swagger.yml | 163 ++++++++ client/docs/swagger-ui/swagger.yaml | 150 ++++++++ client/rest/rest.go | 449 ++++++++++++++++++++++ client/rest/rest_test.go | 461 +++++++++++++++++++++++ x/validator-vesting/client/rest/query.go | 315 ++++++++++++++++ x/validator-vesting/client/rest/rest.go | 12 + 9 files changed, 1566 insertions(+), 1 deletion(-) create mode 100644 client/docs/legacy-swagger.yml create mode 100644 client/rest/rest.go create mode 100644 client/rest/rest_test.go create mode 100644 x/validator-vesting/client/rest/query.go create mode 100644 x/validator-vesting/client/rest/rest.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f73a96b6..4c16f9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog -**As of v0.16.2+, changes are documented in [Kava's Github Releases](https://github.com/Kava-Labs/kava/releases/).** +## Deprecated + +* (x/validator-vesting) [#1542] Deprecate legacy circulating and total supply + rest endpoints. ## [v0.16.1](https://github.com/Kava-Labs/kava/releases/tag/v0.16.1) @@ -151,3 +154,5 @@ Bump tendermint version to 0.32.10 to address [cosmos security advisory Lavender ### Improvements [\#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run large-scale simulations remotely using aws-batch + +[#1542]: https://github.com/Kava-Labs/kava/pulls/1542 diff --git a/app/app.go b/app/app.go index 1ac67522..a0c4590a 100644 --- a/app/app.go +++ b/app/app.go @@ -150,6 +150,7 @@ import ( swapkeeper "github.com/kava-labs/kava/x/swap/keeper" swaptypes "github.com/kava-labs/kava/x/swap/types" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" + validatorvestingrest "github.com/kava-labs/kava/x/validator-vesting/client/rest" validatorvestingtypes "github.com/kava-labs/kava/x/validator-vesting/types" ) @@ -1056,6 +1057,9 @@ func (app *App) SimulationManager() *module.SimulationManager { func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig) { clientCtx := apiSvr.ClientCtx + // Register custom REST routes + validatorvestingrest.RegisterRoutes(clientCtx, apiSvr.Router) + // Register GRPC Gateway routes tmservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) authtx.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) diff --git a/client/docs/config.json b/client/docs/config.json index 3422c14a..86602007 100644 --- a/client/docs/config.json +++ b/client/docs/config.json @@ -267,6 +267,12 @@ } ] } + }, + { + "url": "./client/docs/legacy-swagger.yml", + "dereference": { + "circular": "ignore" + } } ] } diff --git a/client/docs/legacy-swagger.yml b/client/docs/legacy-swagger.yml new file mode 100644 index 00000000..8e00be70 --- /dev/null +++ b/client/docs/legacy-swagger.yml @@ -0,0 +1,163 @@ +--- +swagger: "2.0" +info: + version: "3.0" + title: Kava Light Client RPC + description: A REST interface for state queries, transaction generation and broadcasting. +tags: + - name: Vesting + description: Validator vesting module APIs +schemes: + - https +host: api.data.kava.io +securityDefinitions: + kms: + type: basic +paths: + /vesting/circulatingsupply: + get: + deprecated: true + summary: Get the current circulating supply of KAVA + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: KAVA circulating supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "81443180" + 500: + description: Server internal error + /vesting/totalsupply: + get: + deprecated: true + summary: Get the total supply of KAVA + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: KAVA total supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "120000000" + 500: + description: Server internal error + /vesting/circulatingsupplyhard: + get: + deprecated: true + summary: Get the current circulating supply of HARD + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: HARD circulating supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "63750000" + 500: + description: Server internal error + /vesting/totalsupplyhard: + get: + deprecated: true + summary: Get the total supply of HARD + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: HARD total supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "200000000" + 500: + description: Server internal error + /vesting/circulatingsupplyswp: + get: + deprecated: true + summary: Get the current circulating supply of SWP + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: SWP circulating supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "63750000" + 500: + description: Server internal error + /vesting/circulatingsupplyusdx: + get: + deprecated: true + summary: Get the current circulating supply of USDX + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: USDX circulating supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "63750000" + 500: + description: Server internal error + /vesting/totalsupplyusdx: + get: + deprecated: true + summary: Get the total supply of USDX + tags: + - Vesting + produces: + - application/json + responses: + 200: + description: USDX total supply + schema: + properties: + height: + type: string + example: "100" + result: + type: string + example: "200000000" + 500: + description: Server internal error diff --git a/client/docs/swagger-ui/swagger.yaml b/client/docs/swagger-ui/swagger.yaml index d4c35936..c68d2aab 100644 --- a/client/docs/swagger-ui/swagger.yaml +++ b/client/docs/swagger-ui/swagger.yaml @@ -57868,6 +57868,153 @@ paths: format: boolean tags: - IBC + /vesting/circulatingsupply: + get: + deprecated: true + summary: Get the current circulating supply of KAVA + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: KAVA circulating supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '81443180' + '500': + description: Server internal error + /vesting/totalsupply: + get: + deprecated: true + summary: Get the total supply of KAVA + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: KAVA total supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '120000000' + '500': + description: Server internal error + /vesting/circulatingsupplyhard: + get: + deprecated: true + summary: Get the current circulating supply of HARD + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: HARD circulating supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '63750000' + '500': + description: Server internal error + /vesting/totalsupplyhard: + get: + deprecated: true + summary: Get the total supply of HARD + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: HARD total supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '200000000' + '500': + description: Server internal error + /vesting/circulatingsupplyswp: + get: + deprecated: true + summary: Get the current circulating supply of SWP + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: SWP circulating supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '63750000' + '500': + description: Server internal error + /vesting/circulatingsupplyusdx: + get: + deprecated: true + summary: Get the current circulating supply of USDX + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: USDX circulating supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '63750000' + '500': + description: Server internal error + /vesting/totalsupplyusdx: + get: + deprecated: true + summary: Get the total supply of USDX + tags: + - Vesting + produces: + - application/json + responses: + '200': + description: USDX total supply + schema: + properties: + height: + type: string + example: '100' + result: + type: string + example: '200000000' + '500': + description: Server internal error definitions: cosmos.base.query.v1beta1.PageRequest: type: object @@ -94634,3 +94781,6 @@ definitions: ready to send and receive packets. - STATE_CLOSED: A channel has been closed and can no longer be used to send or receive packets. +securityDefinitions: + kms: + type: basic diff --git a/client/rest/rest.go b/client/rest/rest.go new file mode 100644 index 00000000..79ed92fe --- /dev/null +++ b/client/rest/rest.go @@ -0,0 +1,449 @@ +// Package rest provides HTTP types and primitives for REST +// requests validation and responses handling. +package rest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/legacy" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + DefaultPage = 1 + DefaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 + TxMinHeightKey = "tx.minheight" // Inclusive minimum height filter + TxMaxHeightKey = "tx.maxheight" // Inclusive maximum height filter +) + +// ResponseWithHeight defines a response object type that wraps an original +// response with a height. +type ResponseWithHeight struct { + Height int64 `json:"height"` + Result json.RawMessage `json:"result"` +} + +// NewResponseWithHeight creates a new ResponseWithHeight instance +func NewResponseWithHeight(height int64, result json.RawMessage) ResponseWithHeight { + return ResponseWithHeight{ + Height: height, + Result: result, + } +} + +// ParseResponseWithHeight returns the raw result from a JSON-encoded +// ResponseWithHeight object. +func ParseResponseWithHeight(cdc *codec.LegacyAmino, bz []byte) ([]byte, error) { + r := ResponseWithHeight{} + if err := cdc.UnmarshalJSON(bz, &r); err != nil { + return nil, err + } + + return r.Result, nil +} + +// GasEstimateResponse defines a response definition for tx gas estimation. +type GasEstimateResponse struct { + GasEstimate uint64 `json:"gas_estimate"` +} + +// BaseReq defines a structure that can be embedded in other request structures +// that all share common "base" fields. +type BaseReq struct { + From string `json:"from"` + Memo string `json:"memo"` + ChainID string `json:"chain_id"` + AccountNumber uint64 `json:"account_number"` + Sequence uint64 `json:"sequence"` + TimeoutHeight uint64 `json:"timeout_height"` + Fees sdk.Coins `json:"fees"` + GasPrices sdk.DecCoins `json:"gas_prices"` + Gas string `json:"gas"` + GasAdjustment string `json:"gas_adjustment"` + Simulate bool `json:"simulate"` +} + +// NewBaseReq creates a new basic request instance and sanitizes its values +func NewBaseReq( + from, memo, chainID string, gas, gasAdjustment string, accNumber, seq uint64, + fees sdk.Coins, gasPrices sdk.DecCoins, simulate bool, +) BaseReq { + return BaseReq{ + From: strings.TrimSpace(from), + Memo: strings.TrimSpace(memo), + ChainID: strings.TrimSpace(chainID), + Fees: fees, + GasPrices: gasPrices, + Gas: strings.TrimSpace(gas), + GasAdjustment: strings.TrimSpace(gasAdjustment), + AccountNumber: accNumber, + Sequence: seq, + Simulate: simulate, + } +} + +// Sanitize performs basic sanitization on a BaseReq object. +func (br BaseReq) Sanitize() BaseReq { + return NewBaseReq( + br.From, br.Memo, br.ChainID, br.Gas, br.GasAdjustment, + br.AccountNumber, br.Sequence, br.Fees, br.GasPrices, br.Simulate, + ) +} + +// ValidateBasic performs basic validation of a BaseReq. If custom validation +// logic is needed, the implementing request handler should perform those +// checks manually. +func (br BaseReq) ValidateBasic(w http.ResponseWriter) bool { + if !br.Simulate { + switch { + case len(br.ChainID) == 0: + WriteErrorResponse(w, http.StatusUnauthorized, "chain-id required but not specified") + return false + + case !br.Fees.IsZero() && !br.GasPrices.IsZero(): + // both fees and gas prices were provided + WriteErrorResponse(w, http.StatusBadRequest, "cannot provide both fees and gas prices") + return false + + case !br.Fees.IsValid() && !br.GasPrices.IsValid(): + // neither fees or gas prices were provided + WriteErrorResponse(w, http.StatusPaymentRequired, "invalid fees or gas prices provided") + return false + } + } + + if _, err := sdk.AccAddressFromBech32(br.From); err != nil || len(br.From) == 0 { + WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("invalid from address: %s", br.From)) + return false + } + + return true +} + +// ReadRESTReq reads and unmarshals a Request's body to the the BaseReq struct. +// Writes an error response to ResponseWriter and returns false if errors occurred. +func ReadRESTReq(w http.ResponseWriter, r *http.Request, cdc *codec.LegacyAmino, req interface{}) bool { + body, err := io.ReadAll(r.Body) + if CheckBadRequestError(w, err) { + return false + } + + err = cdc.UnmarshalJSON(body, req) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to decode JSON payload: %s", err)) + return false + } + + return true +} + +// ErrorResponse defines the attributes of a JSON error response. +type ErrorResponse struct { + Code int `json:"code,omitempty"` + Error string `json:"error"` +} + +// NewErrorResponse creates a new ErrorResponse instance. +func NewErrorResponse(code int, err string) ErrorResponse { + return ErrorResponse{Code: code, Error: err} +} + +// CheckError takes care of writing an error response if err is not nil. +// Returns false when err is nil; it returns true otherwise. +func CheckError(w http.ResponseWriter, status int, err error) bool { + if err != nil { + WriteErrorResponse(w, status, err.Error()) + return true + } + + return false +} + +// CheckBadRequestError attaches an error message to an HTTP 400 BAD REQUEST response. +// Returns false when err is nil; it returns true otherwise. +func CheckBadRequestError(w http.ResponseWriter, err error) bool { + return CheckError(w, http.StatusBadRequest, err) +} + +// CheckInternalServerError attaches an error message to an HTTP 500 INTERNAL SERVER ERROR response. +// Returns false when err is nil; it returns true otherwise. +func CheckInternalServerError(w http.ResponseWriter, err error) bool { + return CheckError(w, http.StatusInternalServerError, err) +} + +// CheckNotFoundError attaches an error message to an HTTP 404 NOT FOUND response. +// Returns false when err is nil; it returns true otherwise. +func CheckNotFoundError(w http.ResponseWriter, err error) bool { + return CheckError(w, http.StatusNotFound, err) +} + +// WriteErrorResponse prepares and writes a HTTP error +// given a status code and an error message. +func WriteErrorResponse(w http.ResponseWriter, status int, err string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write(legacy.Cdc.MustMarshalJSON(NewErrorResponse(0, err))) +} + +// WriteSimulationResponse prepares and writes an HTTP +// response for transactions simulations. +func WriteSimulationResponse(w http.ResponseWriter, cdc *codec.LegacyAmino, gas uint64) { + gasEst := GasEstimateResponse{GasEstimate: gas} + + resp, err := cdc.MarshalJSON(gasEst) + if CheckInternalServerError(w, err) { + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(resp) +} + +// ParseUint64OrReturnBadRequest converts s to a uint64 value. +func ParseUint64OrReturnBadRequest(w http.ResponseWriter, s string) (n uint64, ok bool) { + var err error + + n, err = strconv.ParseUint(s, 10, 64) + if err != nil { + WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("'%s' is not a valid uint64", s)) + + return n, false + } + + return n, true +} + +// ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a +// default value, defaultIfEmpty, if the string is empty. +func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEmpty float64) (n float64, ok bool) { + if len(s) == 0 { + return defaultIfEmpty, true + } + + n, err := strconv.ParseFloat(s, 64) + if CheckBadRequestError(w, err) { + return n, false + } + + return n, true +} + +// ParseQueryHeightOrReturnBadRequest sets the height to execute a query if set by the http request. +// It returns false if there was an error parsing the height. +func ParseQueryHeightOrReturnBadRequest(w http.ResponseWriter, clientCtx client.Context, r *http.Request) (client.Context, bool) { + heightStr := r.FormValue("height") + if heightStr != "" { + height, err := strconv.ParseInt(heightStr, 10, 64) + if CheckBadRequestError(w, err) { + return clientCtx, false + } + + if height < 0 { + WriteErrorResponse(w, http.StatusBadRequest, "height must be equal or greater than zero") + return clientCtx, false + } + + if height > 0 { + clientCtx = clientCtx.WithHeight(height) + } + } else { + clientCtx = clientCtx.WithHeight(0) + } + + return clientCtx, true +} + +// PostProcessResponseBare post processes a body similar to PostProcessResponse +// except it does not wrap the body and inject the height. +func PostProcessResponseBare(w http.ResponseWriter, ctx client.Context, body interface{}) { + var ( + resp []byte + err error + ) + + switch b := body.(type) { + case []byte: + resp = b + + default: + resp, err = ctx.LegacyAmino.MarshalJSON(body) + if CheckInternalServerError(w, err) { + return + } + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(resp) +} + +// PostProcessResponse performs post processing for a REST response. The result +// returned to clients will contain two fields, the height at which the resource +// was queried at and the original result. +func PostProcessResponse(w http.ResponseWriter, ctx client.Context, resp interface{}) { + var ( + result []byte + err error + ) + + if ctx.Height < 0 { + WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("negative height in response").Error()) + return + } + + // LegacyAmino used intentionally for REST + marshaler := ctx.LegacyAmino + + switch res := resp.(type) { + case []byte: + result = res + + default: + result, err = marshaler.MarshalJSON(resp) + if CheckInternalServerError(w, err) { + return + } + } + + wrappedResp := NewResponseWithHeight(ctx.Height, result) + + output, err := marshaler.MarshalJSON(wrappedResp) + if CheckInternalServerError(w, err) { + return + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(output) +} + +// ParseHTTPArgsWithLimit parses the request's URL and returns a slice containing +// all arguments pairs. It separates page and limit used for pagination where a +// default limit can be provided. +func ParseHTTPArgsWithLimit(r *http.Request, defaultLimit int) (tags []string, page, limit int, err error) { + tags = make([]string, 0, len(r.Form)) + + for key, values := range r.Form { + if key == "page" || key == "limit" { + continue + } + + var value string + value, err = url.QueryUnescape(values[0]) + + if err != nil { + return tags, page, limit, err + } + + var tag string + + switch key { + case types.TxHeightKey: + tag = fmt.Sprintf("%s=%s", key, value) + + case TxMinHeightKey: + tag = fmt.Sprintf("%s>=%s", types.TxHeightKey, value) + + case TxMaxHeightKey: + tag = fmt.Sprintf("%s<=%s", types.TxHeightKey, value) + + default: + tag = fmt.Sprintf("%s='%s'", key, value) + } + + tags = append(tags, tag) + } + + pageStr := r.FormValue("page") + if pageStr == "" { + page = DefaultPage + } else { + page, err = strconv.Atoi(pageStr) + if err != nil { + return tags, page, limit, err + } else if page <= 0 { + return tags, page, limit, errors.New("page must greater than 0") + } + } + + limitStr := r.FormValue("limit") + if limitStr == "" { + limit = defaultLimit + } else { + limit, err = strconv.Atoi(limitStr) + if err != nil { + return tags, page, limit, err + } else if limit <= 0 { + return tags, page, limit, errors.New("limit must greater than 0") + } + } + + return tags, page, limit, nil +} + +// ParseHTTPArgs parses the request's URL and returns a slice containing all +// arguments pairs. It separates page and limit used for pagination. +func ParseHTTPArgs(r *http.Request) (tags []string, page, limit int, err error) { + return ParseHTTPArgsWithLimit(r, DefaultLimit) +} + +// ParseQueryParamBool parses the given param to a boolean. It returns false by +// default if the string is not parseable to bool. +func ParseQueryParamBool(r *http.Request, param string) bool { + if value, err := strconv.ParseBool(r.FormValue(param)); err == nil { + return value + } + + return false +} + +// GetRequest defines a wrapper around an HTTP GET request with a provided URL. +// An error is returned if the request or reading the body fails. +func GetRequest(url string) ([]byte, error) { + res, err := http.Get(url) //nolint:gosec + if err != nil { + return nil, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if err = res.Body.Close(); err != nil { + return nil, err + } + + return body, nil +} + +// PostRequest defines a wrapper around an HTTP POST request with a provided URL and data. +// An error is returned if the request or reading the body fails. +func PostRequest(url string, contentType string, data []byte) ([]byte, error) { + res, err := http.Post(url, contentType, bytes.NewBuffer(data)) //nolint:gosec + if err != nil { + return nil, fmt.Errorf("error while sending post request: %w", err) + } + + bz, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + if err = res.Body.Close(); err != nil { + return nil, err + } + + return bz, nil +} diff --git a/client/rest/rest_test.go b/client/rest/rest_test.go new file mode 100644 index 00000000..43fd691b --- /dev/null +++ b/client/rest/rest_test.go @@ -0,0 +1,461 @@ +package rest_test + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "sort" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/client/rest" +) + +func TestBaseReq_Sanitize(t *testing.T) { + t.Parallel() + sanitized := rest.BaseReq{ + ChainID: " test", + Memo: "memo ", + From: " cosmos1cq0sxam6x4l0sv9yz3a2vlqhdhvt2k6jtgcse0 ", + Gas: " ", + GasAdjustment: " 0.3", + }.Sanitize() + require.Equal(t, rest.BaseReq{ + ChainID: "test", + Memo: "memo", + From: "cosmos1cq0sxam6x4l0sv9yz3a2vlqhdhvt2k6jtgcse0", + Gas: "", + GasAdjustment: "0.3", + }, sanitized) +} + +func TestBaseReq_ValidateBasic(t *testing.T) { + fromAddr := "cosmos1cq0sxam6x4l0sv9yz3a2vlqhdhvt2k6jtgcse0" + tenstakes, err := types.ParseCoinsNormalized("10stake") + require.NoError(t, err) + onestake, err := types.ParseDecCoins("1.0stake") + require.NoError(t, err) + + req1 := rest.NewBaseReq( + fromAddr, "", "nonempty", "", "", 0, 0, tenstakes, nil, false, + ) + req2 := rest.NewBaseReq( + "", "", "nonempty", "", "", 0, 0, tenstakes, nil, false, + ) + req3 := rest.NewBaseReq( + fromAddr, "", "", "", "", 0, 0, tenstakes, nil, false, + ) + req4 := rest.NewBaseReq( + fromAddr, "", "nonempty", "", "", 0, 0, tenstakes, onestake, false, + ) + req5 := rest.NewBaseReq( + fromAddr, "", "nonempty", "", "", 0, 0, types.Coins{}, types.DecCoins{}, false, + ) + + tests := []struct { + name string + req rest.BaseReq + w http.ResponseWriter + want bool + }{ + {"ok", req1, httptest.NewRecorder(), true}, + {"neither fees nor gasprices provided", req5, httptest.NewRecorder(), true}, + {"empty from", req2, httptest.NewRecorder(), false}, + {"empty chain-id", req3, httptest.NewRecorder(), false}, + {"fees and gasprices provided", req4, httptest.NewRecorder(), false}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, tt.req.ValidateBasic(tt.w)) + }) + } +} + +func TestParseHTTPArgs(t *testing.T) { + t.Parallel() + req0 := mustNewRequest(t, "", "/", nil) + req1 := mustNewRequest(t, "", "/?limit=5", nil) + req2 := mustNewRequest(t, "", "/?page=5", nil) + req3 := mustNewRequest(t, "", "/?page=5&limit=5", nil) + + reqE1 := mustNewRequest(t, "", "/?page=-1", nil) + reqE2 := mustNewRequest(t, "", "/?limit=-1", nil) + req4 := mustNewRequest(t, "", "/?foo=faa", nil) + + reqTxH := mustNewRequest(t, "", "/?tx.minheight=12&tx.maxheight=14", nil) + + tests := []struct { + name string + req *http.Request + w http.ResponseWriter + tags []string + page int + limit int + err bool + }{ + {"no params", req0, httptest.NewRecorder(), []string{}, rest.DefaultPage, rest.DefaultLimit, false}, + {"Limit", req1, httptest.NewRecorder(), []string{}, rest.DefaultPage, 5, false}, + {"Page", req2, httptest.NewRecorder(), []string{}, 5, rest.DefaultLimit, false}, + {"Page and limit", req3, httptest.NewRecorder(), []string{}, 5, 5, false}, + + {"error page 0", reqE1, httptest.NewRecorder(), []string{}, rest.DefaultPage, rest.DefaultLimit, true}, + {"error limit 0", reqE2, httptest.NewRecorder(), []string{}, rest.DefaultPage, rest.DefaultLimit, true}, + + {"tags", req4, httptest.NewRecorder(), []string{"foo='faa'"}, rest.DefaultPage, rest.DefaultLimit, false}, + {"tags", reqTxH, httptest.NewRecorder(), []string{"tx.height<=14", "tx.height>=12"}, rest.DefaultPage, rest.DefaultLimit, false}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tags, page, limit, err := rest.ParseHTTPArgs(tt.req) + + sort.Strings(tags) + + if tt.err { + require.NotNil(t, err) + } else { + require.Nil(t, err) + require.Equal(t, tt.tags, tags) + require.Equal(t, tt.page, page) + require.Equal(t, tt.limit, limit) + } + }) + } +} + +func TestParseQueryHeight(t *testing.T) { + t.Parallel() + var emptyHeight int64 + height := int64(1256756) + + req0 := mustNewRequest(t, "", "/", nil) + req1 := mustNewRequest(t, "", "/?height=1256756", nil) + req2 := mustNewRequest(t, "", "/?height=456yui4567", nil) + req3 := mustNewRequest(t, "", "/?height=-1", nil) + + tests := []struct { + name string + req *http.Request + w http.ResponseWriter + clientCtx client.Context + expectedHeight int64 + expectedOk bool + }{ + {"no height", req0, httptest.NewRecorder(), client.Context{}, emptyHeight, true}, + {"height", req1, httptest.NewRecorder(), client.Context{}, height, true}, + {"invalid height", req2, httptest.NewRecorder(), client.Context{}, emptyHeight, false}, + {"negative height", req3, httptest.NewRecorder(), client.Context{}, emptyHeight, false}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + clientCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(tt.w, tt.clientCtx, tt.req) + if tt.expectedOk { + require.True(t, ok) + require.Equal(t, tt.expectedHeight, clientCtx.Height) + } else { + require.False(t, ok) + require.Empty(t, tt.expectedHeight, clientCtx.Height) + } + }) + } +} + +func TestProcessPostResponse(t *testing.T) { + // mock account + // PubKey field ensures amino encoding is used first since standard + // JSON encoding will panic on cryptotypes.PubKey + + t.Parallel() + type mockAccount struct { + Address types.AccAddress `json:"address"` + Coins types.Coins `json:"coins"` + PubKey cryptotypes.PubKey `json:"public_key"` + AccountNumber uint64 `json:"account_number"` + Sequence uint64 `json:"sequence"` + } + + // setup + viper.Set(flags.FlagOffline, true) + ctx := client.Context{} + height := int64(194423) + + privKey := secp256k1.GenPrivKey() + pubKey := privKey.PubKey() + addr := types.AccAddress(pubKey.Address()) + coins := types.NewCoins(types.NewCoin("atom", types.NewInt(100)), types.NewCoin("tree", types.NewInt(125))) + accNumber := uint64(104) + sequence := uint64(32) + + acc := mockAccount{addr, coins, pubKey, accNumber, sequence} + cdc := codec.NewLegacyAmino() + cryptocodec.RegisterCrypto(cdc) + cdc.RegisterConcrete(&mockAccount{}, "cosmos-sdk/mockAccount", nil) + ctx = ctx.WithLegacyAmino(cdc) + + // setup expected results + jsonNoIndent, err := ctx.LegacyAmino.MarshalJSON(acc) + require.Nil(t, err) + + respNoIndent := rest.NewResponseWithHeight(height, jsonNoIndent) + expectedNoIndent, err := ctx.LegacyAmino.MarshalJSON(respNoIndent) + require.Nil(t, err) + + // check that negative height writes an error + w := httptest.NewRecorder() + ctx = ctx.WithHeight(-1) + rest.PostProcessResponse(w, ctx, acc) + require.Equal(t, http.StatusInternalServerError, w.Code) + + // check that height returns expected response + ctx = ctx.WithHeight(height) + runPostProcessResponse(t, ctx, acc, expectedNoIndent) +} + +func TestReadRESTReq(t *testing.T) { + t.Parallel() + reqBody := io.NopCloser(strings.NewReader(`{"chain_id":"alessio","memo":"text"}`)) + req := &http.Request{Body: reqBody} + w := httptest.NewRecorder() + var br rest.BaseReq + + // test OK + rest.ReadRESTReq(w, req, codec.NewLegacyAmino(), &br) + res := w.Result() //nolint:bodyclose + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, rest.BaseReq{ChainID: "alessio", Memo: "text"}, br) + require.Equal(t, http.StatusOK, res.StatusCode) + + // test non valid JSON + reqBody = io.NopCloser(strings.NewReader(`MALFORMED`)) + req = &http.Request{Body: reqBody} + br = rest.BaseReq{} + w = httptest.NewRecorder() + rest.ReadRESTReq(w, req, codec.NewLegacyAmino(), &br) + require.Equal(t, br, br) + res = w.Result() //nolint:bodyclose + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, http.StatusBadRequest, res.StatusCode) +} + +func TestWriteSimulationResponse(t *testing.T) { + t.Parallel() + w := httptest.NewRecorder() + rest.WriteSimulationResponse(w, codec.NewLegacyAmino(), 10) + res := w.Result() //nolint:bodyclose + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, http.StatusOK, res.StatusCode) + bs, err := io.ReadAll(res.Body) + require.NoError(t, err) + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, `{"gas_estimate":"10"}`, string(bs)) +} + +func TestParseUint64OrReturnBadRequest(t *testing.T) { + t.Parallel() + w := httptest.NewRecorder() + _, ok := rest.ParseUint64OrReturnBadRequest(w, "100") + require.True(t, ok) + require.Equal(t, http.StatusOK, w.Result().StatusCode) //nolint:bodyclose + + w = httptest.NewRecorder() + _, ok = rest.ParseUint64OrReturnBadRequest(w, "-100") + require.False(t, ok) + require.Equal(t, http.StatusBadRequest, w.Result().StatusCode) //nolint:bodyclose +} + +func TestParseFloat64OrReturnBadRequest(t *testing.T) { + t.Parallel() + w := httptest.NewRecorder() + _, ok := rest.ParseFloat64OrReturnBadRequest(w, "100", 0) + require.True(t, ok) + require.Equal(t, http.StatusOK, w.Result().StatusCode) //nolint:bodyclose + + w = httptest.NewRecorder() + _, ok = rest.ParseFloat64OrReturnBadRequest(w, "bad request", 0) + require.False(t, ok) + require.Equal(t, http.StatusBadRequest, w.Result().StatusCode) //nolint:bodyclose + + w = httptest.NewRecorder() + ret, ok := rest.ParseFloat64OrReturnBadRequest(w, "", 9.0) + require.Equal(t, float64(9), ret) + require.True(t, ok) + require.Equal(t, http.StatusOK, w.Result().StatusCode) //nolint:bodyclose +} + +func TestParseQueryParamBool(t *testing.T) { + req := httptest.NewRequest("GET", "/target?boolean=true", nil) + require.True(t, rest.ParseQueryParamBool(req, "boolean")) + require.False(t, rest.ParseQueryParamBool(req, "nokey")) + req = httptest.NewRequest("GET", "/target?boolean=false", nil) + require.False(t, rest.ParseQueryParamBool(req, "boolean")) + require.False(t, rest.ParseQueryParamBool(req, "")) +} + +func TestPostProcessResponseBare(t *testing.T) { + t.Parallel() + + encodingConfig := simappparams.MakeTestEncodingConfig() + clientCtx := client.Context{}. + WithTxConfig(encodingConfig.TxConfig). + WithLegacyAmino(encodingConfig.Amino) // amino used intentionally here + // write bytes + w := httptest.NewRecorder() + bs := []byte("text string") + + rest.PostProcessResponseBare(w, clientCtx, bs) + + res := w.Result() //nolint:bodyclose + require.Equal(t, http.StatusOK, res.StatusCode) + + got, err := io.ReadAll(res.Body) + require.NoError(t, err) + + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, "text string", string(got)) + + // write struct and indent response + w = httptest.NewRecorder() + data := struct { + X int `json:"x"` + S string `json:"s"` + }{X: 10, S: "test"} + + rest.PostProcessResponseBare(w, clientCtx, data) + + res = w.Result() //nolint:bodyclose + require.Equal(t, http.StatusOK, res.StatusCode) + + got, err = io.ReadAll(res.Body) + require.NoError(t, err) + + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, "{\"x\":\"10\",\"s\":\"test\"}", string(got)) + + // write struct, don't indent response + w = httptest.NewRecorder() + data = struct { + X int `json:"x"` + S string `json:"s"` + }{X: 10, S: "test"} + + rest.PostProcessResponseBare(w, clientCtx, data) + + res = w.Result() //nolint:bodyclose + require.Equal(t, http.StatusOK, res.StatusCode) + + got, err = io.ReadAll(res.Body) + require.NoError(t, err) + + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, `{"x":"10","s":"test"}`, string(got)) + + // test marshalling failure + w = httptest.NewRecorder() + data2 := badJSONMarshaller{} + + rest.PostProcessResponseBare(w, clientCtx, data2) + + res = w.Result() //nolint:bodyclose + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + + got, err = io.ReadAll(res.Body) + require.NoError(t, err) + + t.Cleanup(func() { res.Body.Close() }) + require.Equal(t, []string{"application/json"}, res.Header["Content-Type"]) + require.Equal(t, `{"error":"couldn't marshal"}`, string(got)) +} + +type badJSONMarshaller struct{} + +func (badJSONMarshaller) MarshalJSON() ([]byte, error) { + return nil, errors.New("couldn't marshal") +} + +// asserts that ResponseRecorder returns the expected code and body +// runs PostProcessResponse on the objects regular interface and on +// the marshalled struct. +func runPostProcessResponse(t *testing.T, ctx client.Context, obj interface{}, expectedBody []byte) { + // test using regular struct + w := httptest.NewRecorder() + + rest.PostProcessResponse(w, ctx, obj) + require.Equal(t, http.StatusOK, w.Code, w.Body) + + resp := w.Result() //nolint:bodyclose + t.Cleanup(func() { resp.Body.Close() }) + + body, err := io.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, expectedBody, body) + + marshalled, err := ctx.LegacyAmino.MarshalJSON(obj) + require.NoError(t, err) + + // test using marshalled struct + w = httptest.NewRecorder() + rest.PostProcessResponse(w, ctx, marshalled) + + require.Equal(t, http.StatusOK, w.Code, w.Body) + resp = w.Result() //nolint:bodyclose + + t.Cleanup(func() { resp.Body.Close() }) + body, err = io.ReadAll(resp.Body) + + require.Nil(t, err) + require.Equal(t, string(expectedBody), string(body)) +} + +func mustNewRequest(t *testing.T, method, url string, body io.Reader) *http.Request { + req, err := http.NewRequest(method, url, body) + require.NoError(t, err) + err = req.ParseForm() + require.NoError(t, err) + return req +} + +func TestCheckErrors(t *testing.T) { + t.Parallel() + err := errors.New("ERROR") + tests := []struct { + name string + checkerFn func(w http.ResponseWriter, err error) bool + error error + wantErr bool + wantString string + wantStatus int + }{ + {"500", rest.CheckInternalServerError, err, true, `{"error":"ERROR"}`, http.StatusInternalServerError}, + {"500 (no error)", rest.CheckInternalServerError, nil, false, ``, http.StatusInternalServerError}, + {"400", rest.CheckBadRequestError, err, true, `{"error":"ERROR"}`, http.StatusBadRequest}, + {"400 (no error)", rest.CheckBadRequestError, nil, false, ``, http.StatusBadRequest}, + {"404", rest.CheckNotFoundError, err, true, `{"error":"ERROR"}`, http.StatusNotFound}, + {"404 (no error)", rest.CheckNotFoundError, nil, false, ``, http.StatusNotFound}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + require.Equal(t, tt.wantErr, tt.checkerFn(w, tt.error)) + if tt.wantErr { + require.Equal(t, w.Body.String(), tt.wantString) + require.Equal(t, w.Code, tt.wantStatus) + } + }) + } +} diff --git a/x/validator-vesting/client/rest/query.go b/x/validator-vesting/client/rest/query.go new file mode 100644 index 00000000..6be01aab --- /dev/null +++ b/x/validator-vesting/client/rest/query.go @@ -0,0 +1,315 @@ +package rest + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/kava-labs/kava/client/rest" + + "github.com/kava-labs/kava/x/validator-vesting/types" +) + +func registerQueryRoutes(cliCtx client.Context, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/circulatingsupply", types.QueryPath), getCirculatingSupplyHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/totalsupply", types.QueryPath), getTotalSupplyHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/circulatingsupplyhard", types.QueryPath), getCirculatingSupplyHARDHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/circulatingsupplyusdx", types.QueryPath), getCirculatingSupplyUSDXHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/circulatingsupplyswp", types.QueryPath), getCirculatingSupplySWPHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/totalsupplyhard", types.QueryPath), getTotalSupplyHARDHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/totalsupplyusdx", types.QueryPath), getTotalSupplyUSDXHandlerFn(cliCtx)).Methods("GET") +} + +func getTotalSupplyHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryTotalSupply) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + var totalSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &totalSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(totalSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} + +func getCirculatingSupplyHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryCirculatingSupply) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + var circSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} + +func getCirculatingSupplyHARDHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryCirculatingSupplyHARD) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + var circSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} + +func getCirculatingSupplyUSDXHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryCirculatingSupplyUSDX) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + var circSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} + +func getCirculatingSupplySWPHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryCirculatingSupplySWP) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + var circSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(circSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} + +func getTotalSupplyHARDHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryTotalSupplyHARD) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + var totalSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &totalSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(totalSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} + +func getTotalSupplyUSDXHandlerFn(cliCtx client.Context) 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 + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + params := types.NewBaseQueryParams(page, limit) + bz, err := cliCtx.LegacyAmino.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err)) + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryTotalSupplyUSDX) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + var totalSupply int64 + err = cliCtx.LegacyAmino.UnmarshalJSON(res, &totalSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + resBytes, err := json.Marshal(totalSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(resBytes) + } +} diff --git a/x/validator-vesting/client/rest/rest.go b/x/validator-vesting/client/rest/rest.go new file mode 100644 index 00000000..5f4c767a --- /dev/null +++ b/x/validator-vesting/client/rest/rest.go @@ -0,0 +1,12 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client" +) + +// RegisterRoutes registers kavadist-related REST handlers to a router +func RegisterRoutes(cliCtx client.Context, rtr *mux.Router) { + registerQueryRoutes(cliCtx, rtr) +}