diff --git a/node/api/api.go b/node/api/api.go index a31a2ce994..51303684df 100644 --- a/node/api/api.go +++ b/node/api/api.go @@ -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 +} + // 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") +} + +func GetPaginationRequest(req *http.Request, startQueryParam string, limitQueryParam string) PaginationRequest { + 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") + } + slice := reflect.ValueOf(sliceInterface) + if slice.Kind() != reflect.Slice { + 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 { + totalPages += 1 + } + + pagination := PaginationWrapper{ + Start: startingPageIndex, + End: endingPageIndex, + TotalPages: totalPages, + } + + paginationResponse := PaginationResponse{ + Start: startingPageIndex, + Limit: paginationRequest.Limit, + TotalPages: totalPages, + } + return pagination, paginationResponse +} diff --git a/node/api/wallet.go b/node/api/wallet.go index 3edeb92103..540764a13b 100644 --- a/node/api/wallet.go +++ b/node/api/wallet.go @@ -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, + }) + } } // 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 { + 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 diff --git a/types/constants.go b/types/constants.go index a00f7d5004..9814da0228 100644 --- a/types/constants.go +++ b/types/constants.go @@ -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 ) // init checks which build constant is in place and initializes the variables