Skip to content

Commit 8d8fd9d

Browse files
colin-axnerAlessio Treglia
authored andcommitted
generalize query response with height (#4573)
Addition to #4536, no longer specific to account queries. Allows for validator endpoints to return height in the response. Closes: #4609
1 parent 5d5f014 commit 8d8fd9d

File tree

13 files changed

+209
-38
lines changed

13 files changed

+209
-38
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#4573 Returns height in response for query endpoints.

types/rest/rest.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package rest
44

55
import (
6+
"encoding/json"
67
"errors"
78
"fmt"
89
"io/ioutil"
@@ -222,9 +223,17 @@ func ParseQueryHeightOrReturnBadRequest(w http.ResponseWriter, cliCtx context.CL
222223
}
223224

224225
// PostProcessResponse performs post processing for a REST response.
226+
// If the height is greater than zero it will be injected into the body
227+
// of the response. An internal server error is written to the response
228+
// if the height is negative or an encoding/decoding error occurs.
225229
func PostProcessResponse(w http.ResponseWriter, cliCtx context.CLIContext, response interface{}) {
226230
var output []byte
227231

232+
if cliCtx.Height < 0 {
233+
WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("negative height in response").Error())
234+
return
235+
}
236+
228237
switch response.(type) {
229238
case []byte:
230239
output = response.([]byte)
@@ -236,6 +245,32 @@ func PostProcessResponse(w http.ResponseWriter, cliCtx context.CLIContext, respo
236245
} else {
237246
output, err = cliCtx.Codec.MarshalJSON(response)
238247
}
248+
249+
if err != nil {
250+
WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
251+
return
252+
}
253+
}
254+
255+
// inject the height into the response by:
256+
// - decoding into a map
257+
// - adding the height to the map
258+
// - encoding using standard JSON library
259+
if cliCtx.Height > 0 {
260+
m := make(map[string]interface{})
261+
err := json.Unmarshal(output, &m)
262+
if err != nil {
263+
WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
264+
return
265+
}
266+
267+
m["height"] = cliCtx.Height
268+
269+
if cliCtx.Indent {
270+
output, err = json.MarshalIndent(m, "", " ")
271+
} else {
272+
output, err = json.Marshal(m)
273+
}
239274
if err != nil {
240275
WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
241276
return

types/rest/rest_test.go

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
package rest
44

55
import (
6+
"encoding/json"
67
"io"
8+
"io/ioutil"
79
"net/http"
810
"net/http/httptest"
11+
"strconv"
912
"testing"
1013

11-
"github.com/cosmos/cosmos-sdk/client/context"
1214
"github.com/stretchr/testify/require"
15+
"github.com/tendermint/tendermint/crypto"
16+
"github.com/tendermint/tendermint/crypto/secp256k1"
1317

18+
"github.com/cosmos/cosmos-sdk/client/context"
19+
"github.com/cosmos/cosmos-sdk/codec"
1420
"github.com/cosmos/cosmos-sdk/types"
1521
)
1622

@@ -138,6 +144,108 @@ func TestParseQueryHeight(t *testing.T) {
138144
}
139145
}
140146

147+
func TestProcessPostResponse(t *testing.T) {
148+
// mock account
149+
// PubKey field ensures amino encoding is used first since standard
150+
// JSON encoding will panic on crypto.PubKey
151+
type mockAccount struct {
152+
Address types.AccAddress `json:"address"`
153+
Coins types.Coins `json:"coins"`
154+
PubKey crypto.PubKey `json:"public_key"`
155+
AccountNumber uint64 `json:"account_number"`
156+
Sequence uint64 `json:"sequence"`
157+
}
158+
159+
// setup
160+
ctx := context.NewCLIContext()
161+
height := int64(194423)
162+
163+
privKey := secp256k1.GenPrivKey()
164+
pubKey := privKey.PubKey()
165+
addr := types.AccAddress(pubKey.Address())
166+
coins := types.NewCoins(types.NewCoin("atom", types.NewInt(100)), types.NewCoin("tree", types.NewInt(125)))
167+
accNumber := uint64(104)
168+
sequence := uint64(32)
169+
170+
acc := mockAccount{addr, coins, pubKey, accNumber, sequence}
171+
cdc := codec.New()
172+
codec.RegisterCrypto(cdc)
173+
cdc.RegisterConcrete(&mockAccount{}, "cosmos-sdk/mockAccount", nil)
174+
ctx = ctx.WithCodec(cdc)
175+
176+
// setup expected json responses with zero height
177+
jsonNoHeight, err := cdc.MarshalJSON(acc)
178+
require.Nil(t, err)
179+
require.NotNil(t, jsonNoHeight)
180+
jsonIndentNoHeight, err := cdc.MarshalJSONIndent(acc, "", " ")
181+
require.Nil(t, err)
182+
require.NotNil(t, jsonIndentNoHeight)
183+
184+
// decode into map to order alphabetically
185+
m := make(map[string]interface{})
186+
err = json.Unmarshal(jsonNoHeight, &m)
187+
require.Nil(t, err)
188+
jsonMap, err := json.Marshal(m)
189+
require.Nil(t, err)
190+
jsonWithHeight := append(append([]byte(`{"height":`), []byte(strconv.Itoa(int(height))+",")...), jsonMap[1:]...)
191+
jsonIndentMap, err := json.MarshalIndent(m, "", " ")
192+
jsonIndentWithHeight := append(append([]byte(`{`+"\n "+` "height": `), []byte(strconv.Itoa(int(height))+",")...), jsonIndentMap[1:]...)
193+
194+
// check that negative height writes an error
195+
w := httptest.NewRecorder()
196+
ctx = ctx.WithHeight(-1)
197+
PostProcessResponse(w, ctx, acc)
198+
require.Equal(t, http.StatusInternalServerError, w.Code)
199+
200+
// check that zero height returns expected response
201+
ctx = ctx.WithHeight(0)
202+
runPostProcessResponse(t, ctx, acc, jsonNoHeight, false)
203+
// check zero height with indent
204+
runPostProcessResponse(t, ctx, acc, jsonIndentNoHeight, true)
205+
// check that height returns expected response
206+
ctx = ctx.WithHeight(height)
207+
runPostProcessResponse(t, ctx, acc, jsonWithHeight, false)
208+
// check height with indent
209+
runPostProcessResponse(t, ctx, acc, jsonIndentWithHeight, true)
210+
}
211+
212+
// asserts that ResponseRecorder returns the expected code and body
213+
// runs PostProcessResponse on the objects regular interface and on
214+
// the marshalled struct.
215+
func runPostProcessResponse(t *testing.T, ctx context.CLIContext, obj interface{},
216+
expectedBody []byte, indent bool,
217+
) {
218+
if indent {
219+
ctx.Indent = indent
220+
}
221+
222+
// test using regular struct
223+
w := httptest.NewRecorder()
224+
PostProcessResponse(w, ctx, obj)
225+
require.Equal(t, http.StatusOK, w.Code, w.Body)
226+
resp := w.Result()
227+
body, err := ioutil.ReadAll(resp.Body)
228+
require.Nil(t, err)
229+
require.Equal(t, expectedBody, body)
230+
231+
var marshalled []byte
232+
if indent {
233+
marshalled, err = ctx.Codec.MarshalJSONIndent(obj, "", " ")
234+
} else {
235+
marshalled, err = ctx.Codec.MarshalJSON(obj)
236+
}
237+
require.Nil(t, err)
238+
239+
// test using marshalled struct
240+
w = httptest.NewRecorder()
241+
PostProcessResponse(w, ctx, marshalled)
242+
require.Equal(t, http.StatusOK, w.Code, w.Body)
243+
resp = w.Result()
244+
body, err = ioutil.ReadAll(resp.Body)
245+
require.Nil(t, err)
246+
require.Equal(t, expectedBody, body)
247+
}
248+
141249
func mustNewRequest(t *testing.T, method, url string, body io.Reader) *http.Request {
142250
req, err := http.NewRequest(method, url, body)
143251
require.NoError(t, err)

x/auth/client/rest/query.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,9 @@ import (
1111
sdk "github.com/cosmos/cosmos-sdk/types"
1212
"github.com/cosmos/cosmos-sdk/types/rest"
1313
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
14-
"github.com/cosmos/cosmos-sdk/x/auth/exported"
1514
"github.com/cosmos/cosmos-sdk/x/auth/types"
1615
)
1716

18-
// AccountWithHeight wraps the embedded Account with the height it was queried
19-
// at.
20-
type AccountWithHeight struct {
21-
exported.Account `json:"account"`
22-
Height int64 `json:"height"`
23-
}
24-
2517
// query accountREST Handler
2618
func QueryAccountRequestHandlerFn(storeName string, cliCtx context.CLIContext) http.HandlerFunc {
2719

@@ -47,13 +39,14 @@ func QueryAccountRequestHandlerFn(storeName string, cliCtx context.CLIContext) h
4739
return
4840
}
4941

50-
account, err := accGetter.GetAccount(addr)
42+
account, height, err := accGetter.GetAccountWithHeight(addr)
5143
if err != nil {
5244
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
5345
return
5446
}
5547

56-
rest.PostProcessResponse(w, cliCtx, AccountWithHeight{account, cliCtx.Height})
48+
cliCtx = cliCtx.WithHeight(height)
49+
rest.PostProcessResponse(w, cliCtx, account)
5750
}
5851
}
5952

x/auth/types/account_retriever.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,30 @@ func NewAccountRetriever(querier NodeQuerier) AccountRetriever {
2929
// GetAccount queries for an account given an address and a block height. An
3030
// error is returned if the query or decoding fails.
3131
func (ar AccountRetriever) GetAccount(addr sdk.AccAddress) (exported.Account, error) {
32+
account, _, err := ar.GetAccountWithHeight(addr)
33+
return account, err
34+
}
35+
36+
// GetAccountWithHeight queries for an account given an address. Returns the
37+
// height of the query with the account. An error is returned if the query
38+
// or decoding fails.
39+
func (ar AccountRetriever) GetAccountWithHeight(addr sdk.AccAddress) (exported.Account, int64, error) {
3240
bs, err := ModuleCdc.MarshalJSON(NewQueryAccountParams(addr))
3341
if err != nil {
34-
return nil, err
42+
return nil, 0, err
3543
}
3644

37-
res, _, err := ar.querier.QueryWithData(fmt.Sprintf("custom/%s/%s", QuerierRoute, QueryAccount), bs)
45+
res, height, err := ar.querier.QueryWithData(fmt.Sprintf("custom/%s/%s", QuerierRoute, QueryAccount), bs)
3846
if err != nil {
39-
return nil, err
47+
return nil, 0, err
4048
}
4149

4250
var account exported.Account
4351
if err := ModuleCdc.UnmarshalJSON(res, &account); err != nil {
44-
return nil, err
52+
return nil, 0, err
4553
}
4654

47-
return account, nil
55+
return account, height, nil
4856
}
4957

5058
// EnsureExists returns an error if no account exists for the given address else nil.

x/bank/client/rest/query.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ func QueryBalancesRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
3737
return
3838
}
3939

40-
res, _, err := cliCtx.QueryWithData("custom/bank/balances", bz)
40+
res, height, err := cliCtx.QueryWithData("custom/bank/balances", bz)
4141
if err != nil {
4242
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
4343
return
4444
}
4545

46+
cliCtx = cliCtx.WithHeight(height)
47+
4648
// the query will return empty if there is no data for this account
4749
if len(res) == 0 {
4850
rest.PostProcessResponse(w, cliCtx, sdk.Coins{})

x/distribution/client/rest/query.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,13 @@ func delegatorWithdrawalAddrHandlerFn(cliCtx context.CLIContext, queryRoute stri
115115
}
116116

117117
bz := cliCtx.Codec.MustMarshalJSON(types.NewQueryDelegatorWithdrawAddrParams(delegatorAddr))
118-
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/withdraw_addr", queryRoute), bz)
118+
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/withdraw_addr", queryRoute), bz)
119119
if err != nil {
120120
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
121121
return
122122
}
123123

124+
cliCtx = cliCtx.WithHeight(height)
124125
rest.PostProcessResponse(w, cliCtx, res)
125126
}
126127
}
@@ -232,7 +233,7 @@ func communityPoolHandler(cliCtx context.CLIContext, queryRoute string) http.Han
232233
return
233234
}
234235

235-
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/community_pool", queryRoute), nil)
236+
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/community_pool", queryRoute), nil)
236237
if err != nil {
237238
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
238239
return
@@ -244,6 +245,7 @@ func communityPoolHandler(cliCtx context.CLIContext, queryRoute string) http.Han
244245
return
245246
}
246247

248+
cliCtx = cliCtx.WithHeight(height)
247249
rest.PostProcessResponse(w, cliCtx, result)
248250
}
249251
}
@@ -262,12 +264,13 @@ func outstandingRewardsHandlerFn(cliCtx context.CLIContext, queryRoute string) h
262264
}
263265

264266
bin := cliCtx.Codec.MustMarshalJSON(types.NewQueryValidatorOutstandingRewardsParams(validatorAddr))
265-
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/validator_outstanding_rewards", queryRoute), bin)
267+
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/validator_outstanding_rewards", queryRoute), bin)
266268
if err != nil {
267269
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
268270
return
269271
}
270272

273+
cliCtx = cliCtx.WithHeight(height)
271274
rest.PostProcessResponse(w, cliCtx, res)
272275
}
273276
}

x/distribution/types/delegator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
type DelegatorStartingInfo struct {
1515
PreviousPeriod uint64 `json:"previous_period"` // period at which the delegation should withdraw starting from
1616
Stake sdk.Dec `json:"stake"` // amount of staking token delegated
17-
Height uint64 `json:"height"` // height at which delegation was created
17+
Height uint64 `json:"creation_height"` // height at which delegation was created
1818
}
1919

2020
// create a new DelegatorStartingInfo

x/gov/client/rest/rest.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,13 @@ func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
201201
return
202202
}
203203

204-
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/gov/%s/%s", types.QueryParams, paramType), nil)
204+
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/gov/%s/%s", types.QueryParams, paramType), nil)
205205
if err != nil {
206206
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
207207
return
208208
}
209209

210+
cliCtx = cliCtx.WithHeight(height)
210211
rest.PostProcessResponse(w, cliCtx, res)
211212
}
212213
}
@@ -240,12 +241,13 @@ func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
240241
return
241242
}
242243

243-
res, _, err := cliCtx.QueryWithData("custom/gov/proposal", bz)
244+
res, height, err := cliCtx.QueryWithData("custom/gov/proposal", bz)
244245
if err != nil {
245246
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
246247
return
247248
}
248249

250+
cliCtx = cliCtx.WithHeight(height)
249251
rest.PostProcessResponse(w, cliCtx, res)
250252
}
251253
}
@@ -607,12 +609,13 @@ func queryProposalsWithParameterFn(cliCtx context.CLIContext) http.HandlerFunc {
607609
return
608610
}
609611

610-
res, _, err := cliCtx.QueryWithData("custom/gov/proposals", bz)
612+
res, height, err := cliCtx.QueryWithData("custom/gov/proposals", bz)
611613
if err != nil {
612614
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
613615
return
614616
}
615617

618+
cliCtx = cliCtx.WithHeight(height)
616619
rest.PostProcessResponse(w, cliCtx, res)
617620
}
618621
}
@@ -647,12 +650,13 @@ func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
647650
return
648651
}
649652

650-
res, _, err := cliCtx.QueryWithData("custom/gov/tally", bz)
653+
res, height, err := cliCtx.QueryWithData("custom/gov/tally", bz)
651654
if err != nil {
652655
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
653656
return
654657
}
655658

659+
cliCtx = cliCtx.WithHeight(height)
656660
rest.PostProcessResponse(w, cliCtx, res)
657661
}
658662
}

0 commit comments

Comments
 (0)