-
Notifications
You must be signed in to change notification settings - Fork 440
Pagination for /wallet/addresses and /wallet/transactions #2511
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,10 @@ package api | |
|
||
import ( | ||
"encoding/json" | ||
"math" | ||
"net/http" | ||
"reflect" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/NebulousLabs/Sia/build" | ||
|
@@ -29,6 +32,25 @@ type Error struct { | |
// be valid or invalid depending on the current state of a module. | ||
} | ||
|
||
type PaginationRequest struct { | ||
HasPaginationQuery bool | ||
PaginationQueryIsValid bool | ||
Start int | ||
Limit int | ||
} | ||
|
||
type PaginationResponse struct { | ||
Start int `json:"start"` | ||
Limit int `json:"limit"` | ||
TotalPages int `json:"total_pages"` | ||
} | ||
|
||
type PaginationWrapper struct { | ||
Start int | ||
End int | ||
TotalPages int | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are probably enough new pagination types and methods to move the generic ones into a |
||
// Error implements the error interface for the Error type. It returns only the | ||
// Message field. | ||
func (err Error) Error() string { | ||
|
@@ -162,3 +184,77 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) { | |
func WriteSuccess(w http.ResponseWriter) { | ||
w.WriteHeader(http.StatusNoContent) | ||
} | ||
|
||
func GetPaginationDefaultRequest(req *http.Request) PaginationRequest { | ||
return GetPaginationRequest(req, "start", "limit") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about the custom names for |
||
} | ||
|
||
func GetPaginationRequest(req *http.Request, startQueryParam string, limitQueryParam string) PaginationRequest { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you still want to be able to differentiate between a "no pagination params specified" error and a "parsing error" you can declare a custom error for which you can check in the calling method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May I also suggest a different name for the method? |
||
startString := req.FormValue(startQueryParam) | ||
if startString == "" { | ||
return PaginationRequest{ | ||
HasPaginationQuery: false, | ||
PaginationQueryIsValid: false, | ||
Start: 0, | ||
Limit: 0, | ||
} | ||
} | ||
startIndex, err := strconv.Atoi(startString) | ||
if err != nil || startIndex < 0 { | ||
return PaginationRequest{ | ||
HasPaginationQuery: true, | ||
PaginationQueryIsValid: false, | ||
Start: 0, | ||
Limit: 0, | ||
} | ||
} | ||
limit := DefaultPaginationSize | ||
limitString := req.FormValue(limitQueryParam) | ||
if limitString != "" { | ||
limit, err = strconv.Atoi(limitString) | ||
if err != nil || limit <= 0 { | ||
return PaginationRequest{ | ||
HasPaginationQuery: true, | ||
PaginationQueryIsValid: false, | ||
Start: 0, | ||
Limit: 0, | ||
} | ||
} | ||
} | ||
return PaginationRequest{ | ||
HasPaginationQuery: true, | ||
PaginationQueryIsValid: true, | ||
Start: startIndex, | ||
Limit: limit, | ||
} | ||
} | ||
|
||
// returns the starting index, end index, and total pages of a given slice for the page | ||
func GetPaginationIndicesAndResponse(sliceInterface interface{}, paginationRequest PaginationRequest) (PaginationWrapper, PaginationResponse) { | ||
if paginationRequest.Limit <= 0 { | ||
build.Critical("pagination request limit must be greater than 0") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
slice := reflect.ValueOf(sliceInterface) | ||
if slice.Kind() != reflect.Slice { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be a bit complicated for just getting the length of arbitrary data. Why not just change |
||
build.Critical("attempting to paginate on non-slice object type: ", slice.Kind()) | ||
} | ||
startingPageIndex := int(math.Min(float64(paginationRequest.Start), float64(slice.Len()))) | ||
endingPageIndex := int(math.Min(float64(paginationRequest.Start+paginationRequest.Limit), float64(slice.Len()))) | ||
totalPages := slice.Len() / paginationRequest.Limit | ||
if math.Mod(float64(slice.Len()), float64(paginationRequest.Limit)) != 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably don't need |
||
totalPages += 1 | ||
} | ||
|
||
pagination := PaginationWrapper{ | ||
Start: startingPageIndex, | ||
End: endingPageIndex, | ||
TotalPages: totalPages, | ||
} | ||
|
||
paginationResponse := PaginationResponse{ | ||
Start: startingPageIndex, | ||
Limit: paginationRequest.Limit, | ||
TotalPages: totalPages, | ||
} | ||
return pagination, paginationResponse | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,11 @@ type ( | |
Addresses []types.UnlockHash `json:"addresses"` | ||
} | ||
|
||
WalletAddressesPaginatedGET struct { | ||
Addresses []types.UnlockHash `json:"addresses"` | ||
Pagination PaginationResponse `json:"pagination"` | ||
} | ||
|
||
// WalletInitPOST contains the primary seed that gets generated during a | ||
// POST call to /wallet/init. | ||
WalletInitPOST struct { | ||
|
@@ -90,6 +95,13 @@ type ( | |
UnconfirmedTransactions []modules.ProcessedTransaction `json:"unconfirmedtransactions"` | ||
} | ||
|
||
WalletTransactionsPaginatedGET struct { | ||
ConfirmedTransactions []modules.ProcessedTransaction `json:"confirmedtransactions"` | ||
UnconfirmedTransactions []modules.ProcessedTransaction `json:"unconfirmedtransactions"` | ||
ConfirmedTransactionsPagination PaginationResponse `json:"confirmed_transactions_pagination"` | ||
UnconfirmedTransactionsPagination PaginationResponse `json:"unconfirmed_transactions_pagination"` | ||
} | ||
|
||
// WalletTransactionsGETaddr contains the set of wallet transactions | ||
// relevant to the input address provided in the call to | ||
// /wallet/transaction/:addr | ||
|
@@ -179,9 +191,24 @@ func (api *API) walletAddressHandler(w http.ResponseWriter, req *http.Request, _ | |
|
||
// walletAddressHandler handles API calls to /wallet/addresses. | ||
func (api *API) walletAddressesHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { | ||
WriteJSON(w, WalletAddressesGET{ | ||
Addresses: api.wallet.AllAddresses(), | ||
}) | ||
paginationRequest := GetPaginationDefaultRequest(req) | ||
allAddresses := api.wallet.AllAddresses() | ||
if !paginationRequest.HasPaginationQuery || allAddresses == nil { | ||
WriteJSON(w, WalletAddressesGET{ | ||
Addresses: allAddresses, | ||
}) | ||
} else { | ||
if !paginationRequest.PaginationQueryIsValid { | ||
WriteError(w, Error{"start and limit queries must be nonnegative integers"}, http.StatusBadRequest) | ||
return | ||
} | ||
pagination, paginationResponse := GetPaginationIndicesAndResponse(allAddresses, paginationRequest) | ||
addresses := allAddresses[pagination.Start:pagination.End] | ||
WriteJSON(w, WalletAddressesPaginatedGET{ | ||
Addresses: addresses, | ||
Pagination: paginationResponse, | ||
}) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned above I would change the call to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can create a custom error like |
||
} | ||
|
||
// walletBackupHandler handles API calls to /wallet/backup. | ||
|
@@ -521,10 +548,28 @@ func (api *API) walletTransactionsHandler(w http.ResponseWriter, req *http.Reque | |
} | ||
unconfirmedTxns := api.wallet.UnconfirmedTransactions() | ||
|
||
WriteJSON(w, WalletTransactionsGET{ | ||
ConfirmedTransactions: confirmedTxns, | ||
UnconfirmedTransactions: unconfirmedTxns, | ||
}) | ||
confirmedPaginationRequest := GetPaginationRequest(req, "confirmedTransactionsStart", "confirmedTransactionsLimit") | ||
unconfirmedPaginationRequest := GetPaginationRequest(req, "unconfirmedTransactionsStart", "unconfirmedTransactionsLimit") | ||
|
||
if !confirmedPaginationRequest.HasPaginationQuery || !unconfirmedPaginationRequest.HasPaginationQuery || confirmedTxns == nil || unconfirmedTxns == nil { | ||
WriteJSON(w, WalletTransactionsGET{ | ||
ConfirmedTransactions: confirmedTxns, | ||
UnconfirmedTransactions: unconfirmedTxns, | ||
}) | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We try to avoid nested |
||
if !confirmedPaginationRequest.PaginationQueryIsValid || !unconfirmedPaginationRequest.PaginationQueryIsValid { | ||
WriteError(w, Error{"start and limit queries must be nonnegative integers"}, http.StatusBadRequest) | ||
return | ||
} | ||
confirmedPagination, confirmedPaginationResponse := GetPaginationIndicesAndResponse(confirmedTxns, confirmedPaginationRequest) | ||
unconfirmedPagination, unconfirmedPaginationResponse := GetPaginationIndicesAndResponse(unconfirmedTxns, unconfirmedPaginationRequest) | ||
WriteJSON(w, WalletTransactionsPaginatedGET{ | ||
ConfirmedTransactions: confirmedTxns[confirmedPagination.Start:confirmedPagination.End], | ||
UnconfirmedTransactions: unconfirmedTxns[unconfirmedPagination.Start:unconfirmedPagination.End], | ||
ConfirmedTransactionsPagination: confirmedPaginationResponse, | ||
UnconfirmedTransactionsPagination: unconfirmedPaginationResponse, | ||
}) | ||
} | ||
} | ||
|
||
// walletTransactionsAddrHandler handles API calls to | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,9 @@ var ( | |
SiafundCount = NewCurrency64(10000) | ||
SiafundPortion = big.NewRat(39, 1000) | ||
TargetWindow BlockHeight | ||
|
||
// size of each page for paginated API responses | ||
DefaultPaginationSize = 50 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We usually try to keep the constants in a |
||
) | ||
|
||
// init checks which build constant is in place and initializes the variables | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't need that struct at all. We can just return multiple return values in the methods that use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw all exported structs and methods need a comment.