diff --git a/api/accounts.go b/api/accounts.go index e832da44a..04ec9f7a8 100644 --- a/api/accounts.go +++ b/api/accounts.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "encoding/json" "errors" - "strconv" "strings" "time" @@ -53,31 +52,31 @@ func (a *API) enableAccountHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/accounts/{organizationID}/elections/count", + "/accounts/{organizationId}/elections/count", "GET", apirest.MethodAccessTypePublic, - a.electionCountHandler, + a.accountElectionsCountHandler, ); err != nil { return err } if err := a.Endpoint.RegisterMethod( - "/accounts/{organizationID}/elections/status/{status}/page/{page}", + "/accounts/{organizationId}/elections/status/{status}/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.electionListHandler, + a.accountElectionsListByStatusAndPageHandler, ); err != nil { return err } if err := a.Endpoint.RegisterMethod( - "/accounts/{organizationID}/elections/page/{page}", + "/accounts/{organizationId}/elections/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.electionListHandler, + a.accountElectionsListByPageHandler, ); err != nil { return err } if err := a.Endpoint.RegisterMethod( - "/accounts/{accountID}/transfers/page/{page}", + "/accounts/{accountId}/transfers/page/{page}", "GET", apirest.MethodAccessTypePublic, a.tokenTransfersListHandler, @@ -85,7 +84,7 @@ func (a *API) enableAccountHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/accounts/{accountID}/fees/page/{page}", + "/accounts/{accountId}/fees/page/{page}", "GET", apirest.MethodAccessTypePublic, a.tokenFeesHandler, @@ -93,7 +92,7 @@ func (a *API) enableAccountHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/accounts/{accountID}/transfers/count", + "/accounts/{accountId}/transfers/count", "GET", apirest.MethodAccessTypePublic, a.tokenTransfersCountHandler, @@ -112,6 +111,14 @@ func (a *API) enableAccountHandlers() error { "/accounts/page/{page}", "GET", apirest.MethodAccessTypePublic, + a.accountListByPageHandler, + ); err != nil { + return err + } + if err := a.Endpoint.RegisterMethod( + "/accounts", + "GET", + apirest.MethodAccessTypePublic, a.accountListHandler, ); err != nil { return err @@ -130,9 +137,9 @@ func (a *API) enableAccountHandlers() error { // @Produce json // @Param address path string true "Account address" // @Success 200 {object} Account +// @Success 200 {object} AccountMetadata // @Router /accounts/{address} [get] // @Router /accounts/{address}/metadata [get] -// @Success 200 {object} AccountMetadata func (a *API) accountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { if len(util.TrimHex(ctx.URLParam("address"))) != common.AddressLength*2 { return ErrAddressMalformed @@ -311,122 +318,103 @@ func (a *API) accountSetHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContex // @Tags Accounts // @Accept json // @Produce json -// @Success 200 {object} object{count=int} +// @Success 200 {object} CountResult // @Router /accounts/count [get] func (a *API) accountCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { count, err := a.indexer.CountTotalAccounts() if err != nil { return err } + return marshalAndSend(ctx, &CountResult{Count: count}) +} - data, err := json.Marshal( - struct { - Count uint64 `json:"count"` - }{Count: count}, +// accountElectionsListByPageHandler +// +// @Summary List organization elections +// @Description List the elections of an organization +// @Deprecated +// @Description (deprecated, in favor of /elections?page=xxx&organizationId=xxx) +// @Tags Accounts +// @Accept json +// @Produce json +// @Param organizationId path string true "Specific organizationId" +// @Param page path number true "Page" +// @Success 200 {object} ElectionsList +// @Router /accounts/{organizationId}/elections/page/{page} [get] +func (a *API) accountElectionsListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseElectionParams( + ctx.URLParam(ParamPage), + "", + "", + ctx.URLParam(ParamOrganizationId), + "", + "", ) if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) + return err } - return ctx.Send(data, apirest.HTTPstatusOK) + if params.OrganizationID == nil { + return ErrMissingParameter + } + + return a.sendElectionList(ctx, params) } -// electionListHandler +// accountElectionsListByStatusAndPageHandler // -// @Summary List organization elections -// @Description List the elections of an organization +// @Summary List organization elections by status +// @Description List the elections of an organization by status +// @Deprecated +// @Description (deprecated, in favor of /elections?page=xxx&organizationId=xxx&status=xxx) // @Tags Accounts // @Accept json // @Produce json -// @Param organizationID path string true "Specific organizationID" -// @Param page path number true "Define de page number" -// @Success 200 {object} object{elections=[]ElectionSummary} -// @Router /accounts/{organizationID}/elections/page/{page} [get] -// /accounts/{organizationID}/elections/status/{status}/page/{page} [post] Endpoint docs generated on docs/models/model.go -func (a *API) electionListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - organizationID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("organizationID"))) - if err != nil || organizationID == nil { - return ErrCantParseOrgID.Withf("%q", ctx.URLParam("organizationID")) - } - - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } +// @Param organizationId path string true "Specific organizationId" +// @Param status path string true "Election status" Enums(ready, paused, canceled, ended, results) +// @Param page path number true "Page" +// @Success 200 {object} ElectionsList +// @Router /accounts/{organizationId}/elections/status/{status}/page/{page} [get] +func (a *API) accountElectionsListByStatusAndPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseElectionParams( + ctx.URLParam(ParamPage), + "", + ctx.URLParam(ParamStatus), + ctx.URLParam(ParamOrganizationId), + "", + "", + ) + if err != nil { + return err } - page = page * MaxPageSize - var pids [][]byte - switch ctx.URLParam("status") { - case "ready": - pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "READY", false) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - case "paused": - pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "PAUSED", false) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - case "canceled": - pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "CANCELED", false) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - case "ended", "results": - pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "RESULTS", false) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - pids2, err := a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "ENDED", false) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - pids = append(pids, pids2...) - case "": - pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "", false) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - default: - return ErrParamStatusMissing + if params.OrganizationID == nil || params.Status == "" { + return ErrMissingParameter } - elections := []*ElectionSummary{} - for _, pid := range pids { - procInfo, err := a.indexer.ProcessInfo(pid) - if err != nil { - return ErrCantFetchElection.WithErr(err) - } - summary := a.electionSummary(procInfo) - elections = append(elections, &summary) - } - data, err := json.Marshal(&Organization{ - Elections: elections, - }) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - return ctx.Send(data, apirest.HTTPstatusOK) + return a.sendElectionList(ctx, params) } -// electionCountHandler +// accountElectionsCountHandler // // @Summary Count organization elections // @Description Returns the number of elections for an organization // @Tags Accounts // @Accept json // @Produce json -// @Param organizationID path string true "Specific organizationID" -// @Success 200 {object} object{count=number} -// @Router /accounts/{organizationID}/elections/count [get] -func (a *API) electionCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - organizationID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("organizationID"))) - if err != nil || organizationID == nil { - return ErrCantParseOrgID.Withf("%q", ctx.URLParam("organizationID")) +// @Param organizationId path string true "Specific organizationId" +// @Success 200 {object} CountResult +// @Router /accounts/{organizationId}/elections/count [get] +func (a *API) accountElectionsCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + if ctx.URLParam(ParamOrganizationId) == "" { + return ErrMissingParameter + } + + organizationID, err := parseHexString(ctx.URLParam(ParamOrganizationId)) + if err != nil { + return err } + acc, err := a.vocapp.State.GetAccount(common.BytesToAddress(organizationID), true) if acc == nil { return ErrOrgNotFound @@ -434,15 +422,7 @@ func (a *API) electionCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPConte if err != nil { return err } - data, err := json.Marshal( - struct { - Count uint32 `json:"count"` - }{Count: acc.GetProcessIndex()}, - ) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - return ctx.Send(data, apirest.HTTPstatusOK) + return marshalAndSend(ctx, &CountResult{Count: uint64(acc.GetProcessIndex())}) } // tokenTransfersListHandler @@ -452,14 +432,14 @@ func (a *API) electionCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPConte // @Tags Accounts // @Accept json // @Produce json -// @Param accountID path string true "Specific accountID" -// @Param page path string true "Paginator page" +// @Param accountId path string true "Specific accountId" +// @Param page path number true "Page" // @Success 200 {object} object{transfers=indexertypes.TokenTransfersAccount} -// @Router /accounts/{accountID}/transfers/page/{page} [get] +// @Router /accounts/{accountId}/transfers/page/{page} [get] func (a *API) tokenTransfersListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("accountID"))) + accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamAccountId))) if err != nil || accountID == nil { - return ErrCantParseAccountID.Withf("%q", ctx.URLParam("accountID")) + return ErrCantParseAccountID.Withf("%q", ctx.URLParam(ParamAccountId)) } acc, err := a.vocapp.State.GetAccount(common.BytesToAddress(accountID), true) if acc == nil { @@ -468,15 +448,13 @@ func (a *API) tokenTransfersListHandler(_ *apirest.APIdata, ctx *httprouter.HTTP if err != nil { return err } - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } + + page, err := parsePage(ctx.URLParam(ParamPage)) + if err != nil { + return err } - page = page * MaxPageSize - transfers, err := a.indexer.GetTokenTransfersByAccount(accountID, int32(page), MaxPageSize) + + transfers, err := a.indexer.GetTokenTransfersByAccount(accountID, int32(page*DefaultItemsPerPage), DefaultItemsPerPage) if err != nil { return ErrCantFetchTokenTransfers.WithErr(err) } @@ -498,14 +476,14 @@ func (a *API) tokenTransfersListHandler(_ *apirest.APIdata, ctx *httprouter.HTTP // @Tags Accounts // @Accept json // @Produce json -// @Param accountID path string true "Specific accountID" -// @Param page path string true "Paginator page" +// @Param accountId path string true "Specific accountId" +// @Param page path number true "Page" // @Success 200 {object} object{fees=[]indexertypes.TokenFeeMeta} -// @Router /accounts/{accountID}/fees/page/{page} [get] +// @Router /accounts/{accountId}/fees/page/{page} [get] func (a *API) tokenFeesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("accountID"))) + accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamAccountId))) if err != nil || accountID == nil { - return ErrCantParseAccountID.Withf("%q", ctx.URLParam("accountID")) + return ErrCantParseAccountID.Withf("%q", ctx.URLParam(ParamAccountId)) } acc, err := a.vocapp.State.GetAccount(common.BytesToAddress(accountID), true) if acc == nil { @@ -514,16 +492,12 @@ func (a *API) tokenFeesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) if err != nil { return err } - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } + page, err := parsePage(ctx.URLParam(ParamPage)) + if err != nil { + return err } - page = page * MaxPageSize - fees, err := a.indexer.GetTokenFeesByFromAccount(accountID, int32(page), MaxPageSize) + fees, err := a.indexer.GetTokenFeesByFromAccount(accountID, int32(page*DefaultItemsPerPage), DefaultItemsPerPage) if err != nil { return ErrCantFetchTokenTransfers.WithErr(err) } @@ -545,13 +519,13 @@ func (a *API) tokenFeesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) // @Tags Accounts // @Accept json // @Produce json -// @Param accountID path string true "Specific accountID" -// @Success 200 {object} object{count=int} "Number of transaction sent and received for the account" -// @Router /accounts/{accountID}/transfers/count [get] +// @Param accountId path string true "Specific accountId" +// @Success 200 {object} CountResult "Number of transaction sent and received for the account" +// @Router /accounts/{accountId}/transfers/count [get] func (a *API) tokenTransfersCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("accountID"))) + accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamAccountId))) if err != nil || accountID == nil { - return ErrCantParseAccountID.Withf("%q", ctx.URLParam("accountID")) + return ErrCantParseAccountID.Withf("%q", ctx.URLParam(ParamAccountId)) } acc, err := a.vocapp.State.GetAccount(common.BytesToAddress(accountID), true) if acc == nil { @@ -565,16 +539,30 @@ func (a *API) tokenTransfersCountHandler(_ *apirest.APIdata, ctx *httprouter.HTT if err != nil { return err } - data, err := json.Marshal( - struct { - Count uint64 `json:"count"` - }{Count: count}, + return marshalAndSend(ctx, &CountResult{Count: count}) +} + +// accountListByPageHandler +// +// @Summary List of the existing accounts +// @Description Returns information (address, balance and nonce) of the existing accounts. +// @Deprecated +// @Description (deprecated, in favor of /accounts?page=xxx) +// @Tags Accounts +// @Accept json +// @Produce json +// @Param page path number true "Page" +// @Success 200 {object} AccountsList +// @Router /accounts/page/{page} [get] +func (a *API) accountListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseAccountParams( + ctx.URLParam(ParamPage), + "", ) if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) + return err } - - return ctx.Send(data, apirest.HTTPstatusOK) + return a.sendAccountList(ctx, params) } // accountListHandler @@ -584,30 +572,58 @@ func (a *API) tokenTransfersCountHandler(_ *apirest.APIdata, ctx *httprouter.HTT // @Tags Accounts // @Accept json // @Produce json -// @Param page path string true "Paginator page" -// @Success 200 {object} object{accounts=[]indexertypes.Account} -// @Router /accounts/page/{page} [get] +// @Param page query number false "Page" +// @Param limit query number false "Items per page" +// @Success 200 {object} AccountsList +// @Router /accounts [get] func (a *API) accountListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } - } - page = page * MaxPageSize - accounts, err := a.indexer.GetListAccounts(int32(page), MaxPageSize) + params, err := parseAccountParams( + ctx.QueryParam(ParamPage), + ctx.QueryParam(ParamLimit), + ) if err != nil { - return ErrCantFetchTokenTransfers.WithErr(err) + return err } - data, err := json.Marshal( - struct { - Accounts []indexertypes.Account `json:"accounts"` - }{Accounts: accounts}, + return a.sendAccountList(ctx, params) +} + +// sendAccountList produces a paginated AccountsList, +// and sends it marshalled over ctx.Send +// +// Errors returned are always of type APIerror. +func (a *API) sendAccountList(ctx *httprouter.HTTPContext, params *AccountParams) error { + accounts, total, err := a.indexer.AccountsList( + params.Page*params.Limit, + params.Limit, ) if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) + return ErrIndexerQueryFailed.WithErr(err) } - return ctx.Send(data, apirest.HTTPstatusOK) + + if params.Page == 0 && total == 0 { + return ErrAccountNotFound + } + + pagination, err := calculatePagination(params.Page, params.Limit, total) + if err != nil { + return err + } + + list := &AccountsList{ + Accounts: accounts, + Pagination: pagination, + } + return marshalAndSend(ctx, list) +} + +// parseAccountParams returns an AccountParams filled with the passed params +func parseAccountParams(paramPage, paramLimit string) (*AccountParams, error) { + pagination, err := parsePaginationParams(paramPage, paramLimit) + if err != nil { + return nil, err + } + + return &AccountParams{ + PaginationParams: pagination, + }, nil } diff --git a/api/api.go b/api/api.go index 7adc16352..c4a505e8f 100644 --- a/api/api.go +++ b/api/api.go @@ -1,10 +1,7 @@ package api -////// Disabled autoswag due to https://github.com/swaggo/swag/issues/1267 -////// TODO: re-enable when a fixed swaggo/swag is released -////// and remove the workaround done by @selankon on docs/models/models.go -////go:generate go run go.vocdoni.io/dvote/api/autoswag -//go:generate go run github.com/swaggo/swag/cmd/swag@v1.8.10 fmt +//go:generate go run go.vocdoni.io/dvote/api/autoswag +//go:generate go run github.com/swaggo/swag/cmd/swag@v1.16.3 fmt import ( "fmt" @@ -50,8 +47,30 @@ import ( // @securityDefinitions.basic BasicAuth -// MaxPageSize defines the maximum number of results returned by the paginated endpoints -const MaxPageSize = 10 +const ( + // DefaultItemsPerPage defines how many items per page are returned by the paginated endpoints, + // when the client doesn't specify a `limit` param + DefaultItemsPerPage = 10 + // MaxItemsPerPage defines a ceiling for the `limit` param passed by the client + MaxItemsPerPage = 100 +) + +// These consts define the keywords for query (?param=), url (/url/param/) and POST params. +// Note: In JS/TS acronyms like "ID" are camelCased as in "Id". +// +//nolint:revive +const ( + ParamAccountId = "accountId" + ParamCensusId = "censusId" + ParamElectionId = "electionId" + ParamOrganizationId = "organizationId" + ParamVoteId = "voteId" + ParamPage = "page" + ParamLimit = "limit" + ParamStatus = "status" + ParamWithResults = "withResults" + ParamHeight = "height" +) var ( ErrMissingModulesForHandler = fmt.Errorf("missing modules attached for enabling handler") diff --git a/api/api_types.go b/api/api_types.go index 6cdc796ea..a8699e958 100644 --- a/api/api_types.go +++ b/api/api_types.go @@ -12,18 +12,74 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) -type Organization struct { - OrganizationID types.HexBytes `json:"organizationID,omitempty" ` - Elections []*ElectionSummary `json:"elections,omitempty"` - Organizations []*OrganizationList `json:"organizations,omitempty"` - Count *uint64 `json:"count,omitempty" example:"1"` +// ### Params accepted ### + +// PaginationParams allows the client to request a specific page, and how many items per page +type PaginationParams struct { + Page int `json:"page,omitempty"` + Limit int `json:"limit,omitempty"` +} + +// ElectionParams allows the client to filter elections results +type ElectionParams struct { + PaginationParams + OrganizationID types.HexBytes `json:"organizationId,omitempty"` + ElectionID types.HexBytes `json:"electionId,omitempty"` + WithResults bool `json:"withResults,omitempty"` + Status string `json:"status,omitempty"` +} + +// OrganizationParams allows the client to filter organizations results +type OrganizationParams struct { + PaginationParams + OrganizationID types.HexBytes `json:"organizationId,omitempty"` +} + +// AccountParams allows the client to filter accounts results +type AccountParams struct { + PaginationParams +} + +// TransactionParams allows the client to filter transactions results +type TransactionParams struct { + PaginationParams + Height uint64 `json:"height,omitempty"` +} + +// VoteParams allows the client to filter votes +type VoteParams struct { + PaginationParams + ElectionID types.HexBytes `json:"electionId,omitempty"` +} + +// ### Objects returned ### + +// CountResult wraps a count inside an object +type CountResult struct { + Count uint64 `json:"count" example:"10"` } -type OrganizationList struct { +// Pagination contains all the values needed for the UI to easily organize the returned data +type Pagination struct { + TotalItems uint64 `json:"totalItems"` + PreviousPage *uint64 `json:"previousPage"` + CurrentPage uint64 `json:"currentPage"` + NextPage *uint64 `json:"nextPage"` + LastPage uint64 `json:"lastPage"` +} + +type OrganizationSummary struct { OrganizationID types.HexBytes `json:"organizationID" example:"0x370372b92514d81a0e3efb8eba9d036ae0877653"` ElectionCount uint64 `json:"electionCount" example:"1"` } +// OrganizationsList wraps the organizations list to consistently return the list inside an object, +// and return an empty object if the list does not contains any result +type OrganizationsList struct { + Organizations []OrganizationSummary `json:"organizations"` + Pagination *Pagination `json:"pagination"` +} + type ElectionSummary struct { ElectionID types.HexBytes `json:"electionId" ` OrganizationID types.HexBytes `json:"organizationId" ` @@ -37,6 +93,13 @@ type ElectionSummary struct { ChainID string `json:"chainId"` } +// ElectionsList wraps the elections list to consistently return the list inside an object, +// and return an empty object if the list does not contains any result +type ElectionsList struct { + Elections []ElectionSummary `json:"elections"` + Pagination *Pagination `json:"pagination"` +} + // ElectionResults is the struct used to wrap the results of an election type ElectionResults struct { // ABIEncoded is the abi encoded election results @@ -100,13 +163,6 @@ type ElectionDescription struct { TempSIKs bool `json:"tempSIKs"` } -type ElectionFilter struct { - OrganizationID types.HexBytes `json:"organizationId,omitempty" ` - ElectionID types.HexBytes `json:"electionId,omitempty" ` - WithResults *bool `json:"withResults,omitempty"` - Status string `json:"status,omitempty"` -} - type Key struct { Index int `json:"index"` Key types.HexBytes `json:"key" ` @@ -115,7 +171,9 @@ type Key struct { type Vote struct { TxPayload []byte `json:"txPayload,omitempty" extensions:"x-omitempty" swaggerignore:"true"` TxHash types.HexBytes `json:"txHash,omitempty" extensions:"x-omitempty" ` - VoteID types.HexBytes `json:"voteID,omitempty" extensions:"x-omitempty" ` + // VoteID here produces a `voteID` over JSON that differs in casing from the rest of params and JSONs + // but is kept for backwards compatibility + VoteID types.HexBytes `json:"voteID,omitempty" extensions:"x-omitempty" ` // Sent only for encrypted elections (no results until the end) EncryptionKeyIndexes []uint32 `json:"encryptionKeys,omitempty" extensions:"x-omitempty"` // For encrypted elections this will be codified @@ -131,6 +189,11 @@ type Vote struct { Date *time.Time `json:"date,omitempty" extensions:"x-omitempty"` } +type VotesList struct { + Votes []Vote `json:"votes"` + Pagination *Pagination `json:"pagination"` +} + type CensusTypeDescription struct { Type string `json:"type"` Size uint64 `json:"size"` @@ -180,6 +243,7 @@ type TransactionReference struct { Index uint32 `json:"transactionIndex"` } +// TODO: remove this struct, indexertypes.Transaction is the same but includes blockHeight which is great type TransactionMetadata struct { Type string `json:"transactionType"` Number uint32 `json:"transactionNumber"` @@ -187,6 +251,14 @@ type TransactionMetadata struct { Hash types.HexBytes `json:"transactionHash" ` } +// TransactionsList wraps the transactions list to consistently return the list inside an object, +// and return an empty object if the list does not contains any result +type TransactionsList struct { + Transactions []indexertypes.Transaction `json:"transactions"` + Pagination *Pagination `json:"pagination"` +} + +// TODO: this struct should be deprecated, why blockNumber instead of blockHeight?? type BlockTransactionsInfo struct { BlockNumber uint64 `json:"blockNumber"` TransactionsCount uint32 `json:"transactionCount"` @@ -229,6 +301,11 @@ type Account struct { SIK types.HexBytes `json:"sik"` } +type AccountsList struct { + Accounts []indexertypes.Account `json:"accounts"` + Pagination *Pagination `json:"pagination"` +} + type AccountSet struct { TxPayload []byte `json:"txPayload,omitempty" swaggerignore:"true"` Metadata []byte `json:"metadata,omitempty" swaggerignore:"true"` @@ -237,6 +314,8 @@ type AccountSet struct { } type Census struct { + // CensusID here produces a `censusID` over JSON that differs in casing from the rest of params and JSONs + // but is kept for backwards compatibility CensusID types.HexBytes `json:"censusID,omitempty"` Type string `json:"type,omitempty"` Weight *types.BigInt `json:"weight,omitempty"` @@ -339,4 +418,5 @@ func CensusTypeToOrigin(ctype CensusTypeDescription) (models.CensusOrigin, []byt type Block struct { comettypes.Block `json:",inline"` Hash types.HexBytes `json:"hash" ` + TxCount int64 `json:"txCount"` } diff --git a/api/censuses.go b/api/censuses.go index 7a61c2522..fc049bd63 100644 --- a/api/censuses.go +++ b/api/censuses.go @@ -49,7 +49,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/participants", + "/censuses/{censusId}/participants", "POST", apirest.MethodAccessTypePublic, a.censusAddHandler, @@ -57,7 +57,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/type", + "/censuses/{censusId}/type", "GET", apirest.MethodAccessTypePublic, a.censusTypeHandler, @@ -65,7 +65,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/root", + "/censuses/{censusId}/root", "GET", apirest.MethodAccessTypePublic, a.censusRootHandler, @@ -73,7 +73,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/export", + "/censuses/{censusId}/export", "GET", apirest.MethodAccessTypePublic, a.censusDumpHandler, @@ -81,7 +81,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/import", + "/censuses/{censusId}/import", "POST", apirest.MethodAccessTypePublic, a.censusImportHandler, @@ -89,7 +89,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/weight", + "/censuses/{censusId}/weight", "GET", apirest.MethodAccessTypePublic, a.censusWeightHandler, @@ -97,7 +97,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/size", + "/censuses/{censusId}/size", "GET", apirest.MethodAccessTypePublic, a.censusSizeHandler, @@ -105,7 +105,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/publish", + "/censuses/{censusId}/publish", "POST", apirest.MethodAccessTypePublic, a.censusPublishHandler, @@ -113,7 +113,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/publish/async", + "/censuses/{censusId}/publish/async", "POST", apirest.MethodAccessTypePublic, a.censusPublishHandler, @@ -121,7 +121,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/check", + "/censuses/{censusId}/check", "GET", apirest.MethodAccessTypePublic, a.censusPublishCheckHandler, @@ -129,7 +129,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/publish/{root}", + "/censuses/{censusId}/publish/{root}", "POST", apirest.MethodAccessTypePublic, a.censusPublishHandler, @@ -137,7 +137,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}", + "/censuses/{censusId}", "DELETE", apirest.MethodAccessTypePublic, a.censusDeleteHandler, @@ -145,7 +145,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/proof/{key}", + "/censuses/{censusId}/proof/{key}", "GET", apirest.MethodAccessTypePublic, a.censusProofHandler, @@ -153,7 +153,7 @@ func (a *API) enableCensusHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/censuses/{censusID}/verify", + "/censuses/{censusId}/verify", "POST", apirest.MethodAccessTypePublic, a.censusVerifyHandler, @@ -251,16 +251,16 @@ func (a *API) censusCreateHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCont // @Accept json // @Produce json // @Security BasicAuth -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Param transaction body CensusParticipants true "PublicKey - weight array " // @Success 200 "(empty body)" -// @Router /censuses/{censusID}/participants [post] +// @Router /censuses/{censusId}/participants [post] func (a *API) censusAddHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { token, err := uuid.Parse(msg.AuthToken) if err != nil { return err } - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -341,11 +341,11 @@ func (a *API) censusAddHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext // @Tags Censuses // @Accept json // @Produce json -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 {object} object{census=string} "Census type "weighted", "zkweighted", "csp" -// @Router /censuses/{censusID}/type [get] +// @Router /censuses/{censusId}/type [get] func (a *API) censusTypeHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -376,11 +376,11 @@ func (a *API) censusTypeHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) // @Tags Censuses // @Accept json // @Produce json -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 {object} object{root=string} "Merkle root of the census" -// @Router /censuses/{censusID}/root [get] +// @Router /censuses/{censusId}/root [get] func (a *API) censusRootHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -414,15 +414,15 @@ func (a *API) censusRootHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) // @Accept json // @Produce json // @Security BasicAuth -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 {object} censusdb.CensusDump -// @Router /censuses/{censusID}/export [get] +// @Router /censuses/{censusId}/export [get] func (a *API) censusDumpHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { token, err := uuid.Parse(msg.AuthToken) if err != nil { return err } - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -462,15 +462,15 @@ func (a *API) censusDumpHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContex // @Accept json // @Produce json // @Security BasicAuth -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 "(empty body)" -// @Router /censuses/{censusID}/import [post] +// @Router /censuses/{censusId}/import [post] func (a *API) censusImportHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { token, err := uuid.Parse(msg.AuthToken) if err != nil { return err } - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -518,11 +518,11 @@ func (a *API) censusImportHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCont // @Tags Censuses // @Accept json // @Produce json -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 {object} object{weight=string} "Sum of weight son a stringfied big int format" -// @Router /censuses/{censusID}/weight [get] +// @Router /censuses/{censusId}/weight [get] func (a *API) censusWeightHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -555,11 +555,11 @@ func (a *API) censusWeightHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContex // @Tags Censuses // @Accept json // @Produce json -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 {object} object{size=string} "Size as integer" -// @Router /censuses/{censusID}/size [get] +// @Router /censuses/{censusId}/size [get] func (a *API) censusSizeHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -595,15 +595,15 @@ func (a *API) censusSizeHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) // @Tags Censuses // @Accept json // @Produce json -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 "(empty body)" -// @Router /censuses/{censusID} [delete] +// @Router /censuses/{censusId} [delete] func (a *API) censusDeleteHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { token, err := uuid.Parse(msg.AuthToken) if err != nil { return err } - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -630,15 +630,17 @@ func (a *API) censusDeleteHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCont // @Produce json // @Security BasicAuth // @Success 200 {object} object{census=object{censusID=string,uri=string}} "It return published censusID and the ipfs uri where its uploaded" -// @Param censusID path string true "Census id" -// @Router /censuses/{censusID}/publish [post] -// @Router /censuses/{censusID}/publish/async [post] +// @Param censusId path string true "Census id" +// @Param root path string false "Specific root where to publish the census. Not required" +// @Router /censuses/{censusId}/publish [post] +// @Router /censuses/{censusId}/publish/async [post] +// @Router /censuses/{censusId}/publish/{root} [post] func (a *API) censusPublishHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { token, err := uuid.Parse(msg.AuthToken) if err != nil { return err } - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -780,10 +782,10 @@ func (a *API) censusPublishHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCon // @Tags Censuses // @Produce json // @Success 200 {object} object{census=object{censusID=string,uri=string}} "It return published censusID and the ipfs uri where its uploaded" -// @Param censusID path string true "Census id" -// @Router /censuses/{censusID}/check [get] +// @Param censusId path string true "Census id" +// @Router /censuses/{censusId}/check [get] func (a *API) censusPublishCheckHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -817,12 +819,12 @@ func (a *API) censusPublishCheckHandler(_ *apirest.APIdata, ctx *httprouter.HTTP // @Accept json // @Produce json // @Security BasicAuth -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Param key path string true "Key to proof" // @Success 200 {object} object{weight=number,proof=string,value=string} "where proof is Merkle tree siblings and value is Merkle tree leaf value" -// @Router /censuses/{censusID}/proof/{key} [get] +// @Router /censuses/{censusId}/proof/{key} [get] func (a *API) censusProofHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -892,11 +894,11 @@ func (a *API) censusProofHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext // @Tags Censuses // @Accept json // @Produce json -// @Param censusID path string true "Census id" +// @Param censusId path string true "Census id" // @Success 200 {object} object{valid=bool} -// @Router /censuses/{censusID}/verify [post] +// @Router /censuses/{censusId}/verify [post] func (a *API) censusVerifyHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { - censusID, err := censusIDparse(ctx.URLParam("censusID")) + censusID, err := censusIDparse(ctx.URLParam(ParamCensusId)) if err != nil { return err } @@ -957,7 +959,7 @@ func (a *API) censusVerifyHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCont // @Accept json // @Produce json // @Success 200 {object} object{valid=bool} -// @Router /censuses/list/ [get] +// @Router /censuses/list [get] func (a *API) censusListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { list, err := a.censusdb.List() if err != nil { @@ -979,7 +981,8 @@ func (a *API) censusListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) // @Produce json // @Param ipfs path string true "Export to IPFS. Blank to return the JSON file" // @Success 200 {object} object{valid=bool} -// @Router /censuses/export/{ipfs} [get] +// @Router /censuses/export/ipfs [get] +// @Router /censuses/export [get] func (a *API) censusExportDBHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { isIPFSExport := strings.HasSuffix(ctx.Request.URL.Path, "ipfs") buf := bytes.Buffer{} @@ -1012,7 +1015,8 @@ func (a *API) censusExportDBHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCont // @Accept json // @Produce json // @Success 200 {object} object{valid=bool} -// @Router /censuses/import/{ipfscid} [post] +// @Router /censuses/import/{ipfscid} [get] +// @Router /censuses/import [post] func (a *API) censusImportDBHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { ipfscid := ctx.URLParam("ipfscid") if ipfscid == "" { diff --git a/api/chain.go b/api/chain.go index 858d48408..5330e760b 100644 --- a/api/chain.go +++ b/api/chain.go @@ -13,7 +13,6 @@ import ( "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/httprouter" "go.vocdoni.io/dvote/httprouter/apirest" - "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/util" "go.vocdoni.io/dvote/vochain" "go.vocdoni.io/dvote/vochain/genesis" @@ -29,13 +28,21 @@ const ( func (a *API) enableChainHandlers() error { if err := a.Endpoint.RegisterMethod( - "/chain/organizations/page/{page}", + "/chain/organizations", "GET", apirest.MethodAccessTypePublic, a.organizationListHandler, ); err != nil { return err } + if err := a.Endpoint.RegisterMethod( + "/chain/organizations/page/{page}", + "GET", + apirest.MethodAccessTypePublic, + a.organizationListByPageHandler, + ); err != nil { + return err + } if err := a.Endpoint.RegisterMethod( "/chain/organizations/count", "GET", @@ -112,7 +119,7 @@ func (a *API) enableChainHandlers() error { "/chain/blocks/{height}/transactions/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.chainTxByHeightHandler, + a.chainTxListByHeightAndPageHandler, ); err != nil { return err } @@ -136,7 +143,7 @@ func (a *API) enableChainHandlers() error { "/chain/transactions/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.chainTxListPaginated, + a.chainTxListByPageHandler, ); err != nil { return err } @@ -168,7 +175,7 @@ func (a *API) enableChainHandlers() error { "/chain/organizations/filter/page/{page}", "POST", apirest.MethodAccessTypePublic, - a.chainOrganizationsFilterPaginatedHandler, + a.organizationListByFilterAndPageHandler, ); err != nil { return err } @@ -223,39 +230,114 @@ func (a *API) enableChainHandlers() error { // @Tags Chain // @Accept json // @Produce json -// @Param page path int true "Page number" -// @Success 200 {object} api.organizationListHandler.response -// @Router /chain/organizations/page/{page} [get] +// @Param page query number false "Page" +// @Param limit query number false "Items per page" +// @Param organizationId query string false "Filter by partial organizationId" +// @Success 200 {object} OrganizationsList +// @Router /chain/organizations [get] func (a *API) organizationListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } + params, err := parseOrganizationParams( + ctx.QueryParam(ParamPage), + ctx.QueryParam(ParamLimit), + ctx.QueryParam(ParamOrganizationId), + ) + if err != nil { + return err } - page = page * MaxPageSize - organizations := []*OrganizationList{} + return a.sendOrganizationList(ctx, params) +} - list := a.indexer.EntityList(MaxPageSize, page, "") - for _, org := range list { - organizations = append(organizations, &OrganizationList{ - OrganizationID: org.EntityID, - ElectionCount: uint64(org.ProcessCount), - }) +// organizationListByPageHandler +// +// @Summary List organizations +// @Description List all organizations +// @Deprecated +// @Description (deprecated, in favor of /chain/organizations?page=xxx) +// @Tags Chain +// @Accept json +// @Produce json +// @Param page path number true "Page" +// @Success 200 {object} OrganizationsList +// @Router /chain/organizations/page/{page} [get] +func (a *API) organizationListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseOrganizationParams( + ctx.URLParam(ParamPage), + "", + "", + ) + if err != nil { + return err + } + return a.sendOrganizationList(ctx, params) +} + +// organizationListByFilterAndPageHandler +// +// @Summary List organizations (filtered) +// @Description Returns a list of organizations filtered by its partial id, paginated by the given page +// @Deprecated +// @Description (deprecated, in favor of /chain/organizations?page=xxx&organizationId=xxx) +// @Tags Chain +// @Accept json +// @Produce json +// @Param body body OrganizationParams true "Partial organizationId to filter by" +// @Param page path number true "Page" +// @Success 200 {object} OrganizationsList +// @Router /chain/organizations/filter/page/{page} [post] +func (a *API) organizationListByFilterAndPageHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { + // get organizationId from the request params + params := &OrganizationParams{} + + // but support legacy URLParam + urlParams, err := parsePaginationParams(ctx.URLParam(ParamPage), "") + if err != nil { + return err + } + params.PaginationParams = urlParams + + if err := json.Unmarshal(msg.Data, ¶ms); err != nil { + return ErrCantParseDataAsJSON.WithErr(err) + } + if params == nil { // happens when client POSTs a literal `null` JSON + return ErrMissingParameter + } + + return a.sendOrganizationList(ctx, params) +} + +// sendOrganizationList produces a filtered, paginated OrganizationsList, +// and sends it marshalled over ctx.Send +// +// Errors returned are always of type APIerror. +func (a *API) sendOrganizationList(ctx *httprouter.HTTPContext, params *OrganizationParams) error { + orgs, total, err := a.indexer.EntityList( + params.Page*params.Limit, + params.Limit, + params.OrganizationID.String(), + ) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) } - type response struct { - Organizations []*OrganizationList `json:"organizations"` + if params.Page == 0 && total == 0 { + return ErrOrgNotFound } - data, err := json.Marshal(response{organizations}) + pagination, err := calculatePagination(params.Page, params.Limit, total) if err != nil { return err } - return ctx.Send(data, apirest.HTTPstatusOK) + list := &OrganizationsList{ + Pagination: pagination, + } + for _, org := range orgs { + list.Organizations = append(list.Organizations, OrganizationSummary{ + OrganizationID: org.EntityID, + ElectionCount: uint64(org.ProcessCount), + }) + } + return marshalAndSend(ctx, list) } // organizationCountHandler @@ -265,16 +347,11 @@ func (a *API) organizationListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCo // @Tags Chain // @Accept json // @Produce json -// @Success 200 {object} object{count=int} "Number of registered organizations" +// @Success 200 {object} CountResult "Number of registered organizations" // @Router /chain/organizations/count [get] func (a *API) organizationCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { count := a.indexer.CountTotalEntities() - organization := &Organization{Count: &count} - data, err := json.Marshal(organization) - if err != nil { - return err - } - return ctx.Send(data, apirest.HTTPstatusOK) + return marshalAndSend(ctx, &CountResult{Count: count}) } // chainInfoHandler @@ -502,43 +579,29 @@ func (a *API) chainTxCostHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext return ctx.Send(data, apirest.HTTPstatusOK) } -// chainTxListPaginated +// chainTxListByPageHandler // -// @Summary List Transactions +// @Summary List transactions // @Description To get full transaction information use [/chain/transaction/{blockHeight}/{txIndex}](transaction-by-block-index).\nWhere transactionIndex is the index of the transaction on the containing block. +// @Deprecated +// @Description (deprecated, in favor of /chain/transactions?page=xxx) // @Tags Chain // @Accept json // @Produce json -// @Param page path int true "Page number" -// @Success 200 {object} api.chainTxListPaginated.response "It return a list of transactions references" +// @Param page path number true "Page" +// @Success 200 {object} TransactionsList "List of transactions references" // @Router /chain/transactions/page/{page} [get] -func (a *API) chainTxListPaginated(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - page := 0 - if ctx.URLParam("page") != "" { - var err error - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return err - } - } - offset := int32(page * MaxPageSize) - refs, err := a.indexer.GetLastTransactions(MaxPageSize, offset) - if err != nil { - if errors.Is(err, indexer.ErrTransactionNotFound) { - return ErrTransactionNotFound - } - return err - } - // wrap list in a struct to consistently return list in an object, return empty - // object if the list does not contains any result - type response struct { - Txs []*indexertypes.Transaction `json:"transactions"` - } - data, err := json.Marshal(response{refs}) +func (a *API) chainTxListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseTransactionParams( + ctx.URLParam(ParamPage), + "", + "", + ) if err != nil { return err } - return ctx.Send(data, apirest.HTTPstatusOK) + + return a.sendTransactionList(ctx, params) } // chainTxbyHashHandler @@ -650,7 +713,7 @@ func (a *API) chainTxByIndexHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCont return ctx.Send(data, apirest.HTTPstatusOK) } -// chainTxByHeightHandler +// chainTxListByHeightAndPageHandler // // @Summary Transactions in a block // @Description Given a block returns the list of transactions for that block @@ -658,35 +721,34 @@ func (a *API) chainTxByIndexHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCont // @Accept json // @Produce json // @Param height path number true "Block height" -// @Param page path number true "Page to paginate" +// @Param page path number true "Page" // @Success 200 {object} []TransactionMetadata // @Router /chain/blocks/{height}/transactions/page/{page} [get] -func (a *API) chainTxByHeightHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - height, err := strconv.ParseUint(ctx.URLParam("height"), 10, 64) +func (a *API) chainTxListByHeightAndPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseTransactionParams( + ctx.URLParam(ParamPage), + "", + ctx.URLParam(ParamHeight), + ) if err != nil { return err } - block := a.vocapp.GetBlockByHeight(int64(height)) + + // TODO: replace all of this with the indexer method, + // from the other PR, TransactionListByHeight + block := a.vocapp.GetBlockByHeight(int64(params.Height)) if block == nil { return ErrBlockNotFound } blockTxs := &BlockTransactionsInfo{ - BlockNumber: height, + BlockNumber: params.Height, TransactionsCount: uint32(len(block.Txs)), Transactions: make([]TransactionMetadata, 0), } - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber.WithErr(err) - } - } - page = page * MaxPageSize count := 0 - for i := page; i < len(block.Txs); i++ { - if count >= MaxPageSize { + for i := params.Page * params.Limit; i < len(block.Txs); i++ { + if count >= params.Limit { break } signedTx := new(models.SignedTx) @@ -722,6 +784,38 @@ func (a *API) chainTxByHeightHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCon return ctx.Send(data, apirest.HTTPstatusOK) } +// sendTransactionList produces a filtered, paginated TransactionList, +// and sends it marshalled over ctx.Send +// +// Errors returned are always of type APIerror. +func (a *API) sendTransactionList(ctx *httprouter.HTTPContext, params *TransactionParams) error { + txs, total, err := a.indexer.GetLastTransactions( + int32(params.Limit), + int32(params.Page*params.Limit), + ) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) + } + + if params.Page == 0 && total == 0 { + return ErrTransactionNotFound + } + + pagination, err := calculatePagination(params.Page, params.Limit, total) + if err != nil { + return err + } + + list := &TransactionsList{ + Pagination: pagination, + } + for _, tx := range txs { + list.Transactions = append(list.Transactions, *tx) + } + + return marshalAndSend(ctx, list) +} + // chainValidatorsHandler // // @Summary List validators @@ -772,18 +866,31 @@ func (a *API) chainBlockHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) if err != nil { return err } - tmblock := a.vocapp.GetBlockByHeight(int64(height)) - if tmblock == nil { - return ErrBlockNotFound + idxblock, err := a.indexer.BlockByHeight(int64(height)) + if err != nil { + if errors.Is(err, indexer.ErrBlockNotFound) { + return ErrBlockNotFound + } + return ErrBlockNotFound.WithErr(err) + } + txcount, err := a.indexer.CountTransactionsByHeight(int64(height)) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) } block := &Block{ Block: comettypes.Block{ - Header: tmblock.Header, - Data: tmblock.Data, - Evidence: tmblock.Evidence, - LastCommit: tmblock.LastCommit, + Header: comettypes.Header{ + ChainID: idxblock.ChainID, + Height: idxblock.Height, + Time: idxblock.Time, + ProposerAddress: []byte(idxblock.ProposerAddress), + LastBlockID: comettypes.BlockID{ + Hash: []byte(idxblock.LastBlockHash), + }, + }, }, - Hash: types.HexBytes(tmblock.Hash()), + Hash: idxblock.Hash, + TxCount: txcount, } data, err := json.Marshal(block) if err != nil { @@ -807,18 +914,31 @@ func (a *API) chainBlockByHashHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCo if err != nil { return err } - tmblock := a.vocapp.GetBlockByHash(hash) - if tmblock == nil { - return ErrBlockNotFound + idxblock, err := a.indexer.BlockByHash(hash) + if err != nil { + if errors.Is(err, indexer.ErrBlockNotFound) { + return ErrBlockNotFound + } + return ErrBlockNotFound.WithErr(err) + } + txcount, err := a.indexer.CountTransactionsByHeight(idxblock.Height) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) } block := &Block{ Block: comettypes.Block{ - Header: tmblock.Header, - Data: tmblock.Data, - Evidence: tmblock.Evidence, - LastCommit: tmblock.LastCommit, + Header: comettypes.Header{ + ChainID: idxblock.ChainID, + Height: idxblock.Height, + Time: idxblock.Time, + ProposerAddress: []byte(idxblock.ProposerAddress), + LastBlockID: comettypes.BlockID{ + Hash: []byte(idxblock.LastBlockHash), + }, + }, }, - Hash: types.HexBytes(tmblock.Hash()), + Hash: idxblock.Hash, + TxCount: txcount, } data, err := json.Marshal(block) if err != nil { @@ -827,59 +947,6 @@ func (a *API) chainBlockByHashHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCo return ctx.Send(convertKeysToCamel(data), apirest.HTTPstatusOK) } -// chainOrganizationsFilterPaginatedHandler -// -// @Summary List organizations (filtered) -// @Description Returns a list of organizations filtered by its partial id, paginated by the given page -// @Tags Chain -// @Accept json -// @Produce json -// @Param organizationId body object{organizationId=string} true "Partial organizationId to filter by" -// @Param page path int true "Current page" -// @Success 200 {object} object{organizations=[]api.OrganizationList} -// @Router /chain/organizations/filter/page/{page} [post] -func (a *API) chainOrganizationsFilterPaginatedHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { - // get organizationId from the request body - requestData := struct { - OrganizationId string `json:"organizationId"` - }{} - if err := json.Unmarshal(msg.Data, &requestData); err != nil { - return ErrCantParseDataAsJSON.WithErr(err) - } - // get page - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber.WithErr(err) - } - } - page = page * MaxPageSize - - organizations := []*OrganizationList{} - // get matching organization ids from the indexer - matchingOrganizationIds := a.indexer.EntityList(MaxPageSize, page, util.TrimHex(requestData.OrganizationId)) - if len(matchingOrganizationIds) == 0 { - return ErrOrgNotFound - } - - for _, org := range matchingOrganizationIds { - organizations = append(organizations, &OrganizationList{ - OrganizationID: org.EntityID, - ElectionCount: uint64(org.ProcessCount), - }) - } - - data, err := json.Marshal(struct { - Organizations []*OrganizationList `json:"organizations"` - }{organizations}) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - return ctx.Send(data, apirest.HTTPstatusOK) -} - // chainTransactionCountHandler // // @Summary Transactions count @@ -887,24 +954,14 @@ func (a *API) chainOrganizationsFilterPaginatedHandler(msg *apirest.APIdata, ctx // @Tags Chain // @Accept json // @Produce json -// @Success 200 {object} uint64 -// @Success 200 {object} object{count=number} +// @Success 200 {object} CountResult // @Router /chain/transactions/count [get] func (a *API) chainTxCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { count, err := a.indexer.CountTotalTransactions() if err != nil { return err } - data, err := json.Marshal( - struct { - Count uint64 `json:"count"` - }{Count: count}, - ) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - - return ctx.Send(data, apirest.HTTPstatusOK) + return marshalAndSend(ctx, &CountResult{Count: count}) } // chainListFeesHandler @@ -914,21 +971,16 @@ func (a *API) chainTxCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContex // @Tags Accounts // @Accept json // @Produce json -// @Param page path string true "Paginator page" +// @Param page path number true "Page" // @Success 200 {object} object{fees=[]indexertypes.TokenFeeMeta} // @Router /chain/fees/page/{page} [get] func (a *API) chainListFeesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } + page, err := parsePage(ctx.URLParam(ParamPage)) + if err != nil { + return err } - page = page * MaxPageSize - fees, err := a.indexer.GetTokenFees(int32(page), MaxPageSize) + fees, err := a.indexer.GetTokenFees(int32(page*DefaultItemsPerPage), DefaultItemsPerPage) if err != nil { return ErrCantFetchTokenTransfers.WithErr(err) } @@ -951,26 +1003,21 @@ func (a *API) chainListFeesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPConte // @Accept json // @Produce json // @Param reference path string true "Reference filter" -// @Param page path string true "Paginator page" +// @Param page path number true "Page" // @Success 200 {object} object{fees=[]indexertypes.TokenFeeMeta} // @Router /chain/fees/reference/{reference}/page/{page} [get] func (a *API) chainListFeesByReferenceHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } + page, err := parsePage(ctx.URLParam(ParamPage)) + if err != nil { + return err } - page = page * MaxPageSize reference := ctx.URLParam("reference") if reference == "" { return ErrMissingParameter } - fees, err := a.indexer.GetTokenFeesByReference(reference, int32(page), MaxPageSize) + fees, err := a.indexer.GetTokenFeesByReference(reference, int32(page*DefaultItemsPerPage), DefaultItemsPerPage) if err != nil { return ErrCantFetchTokenTransfers.WithErr(err) } @@ -993,26 +1040,21 @@ func (a *API) chainListFeesByReferenceHandler(_ *apirest.APIdata, ctx *httproute // @Accept json // @Produce json // @Param type path string true "Type filter" -// @Param page path string true "Paginator page" +// @Param page path number true "Page" // @Success 200 {object} object{fees=[]indexertypes.TokenFeeMeta} // @Router /chain/fees/type/{type}/page/{page} [get] func (a *API) chainListFeesByTypeHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } + page, err := parsePage(ctx.URLParam(ParamPage)) + if err != nil { + return err } - page = page * MaxPageSize typeFilter := ctx.URLParam("type") if typeFilter == "" { return ErrMissingParameter } - fees, err := a.indexer.GetTokenFeesByType(typeFilter, int32(page), MaxPageSize) + fees, err := a.indexer.GetTokenFeesByType(typeFilter, int32(page*DefaultItemsPerPage), DefaultItemsPerPage) if err != nil { return ErrCantFetchTokenTransfers.WithErr(err) } @@ -1044,3 +1086,39 @@ func (a *API) chainIndexerExportHandler(_ *apirest.APIdata, ctx *httprouter.HTTP } return ctx.Send(data, apirest.HTTPstatusOK) } + +// parseOrganizationParams returns an OrganizationParams filled with the passed params +func parseOrganizationParams(paramPage, paramLimit, paramOrganizationId string) (*OrganizationParams, error) { + pagination, err := parsePaginationParams(paramPage, paramLimit) + if err != nil { + return nil, err + } + + organizationId, err := parseHexString(paramOrganizationId) + if err != nil { + return nil, err + } + + return &OrganizationParams{ + PaginationParams: pagination, + OrganizationID: organizationId, + }, nil +} + +// parseTransactionParams returns an TransactionParams filled with the passed params +func parseTransactionParams(paramPage, paramLimit, paramHeight string) (*TransactionParams, error) { + pagination, err := parsePaginationParams(paramPage, paramLimit) + if err != nil { + return nil, err + } + + height, err := parseNumber(paramHeight) + if err != nil { + return nil, err + } + + return &TransactionParams{ + PaginationParams: pagination, + Height: uint64(height), + }, nil +} diff --git a/api/docs/descriptions/electionFilterPaginatedHandler.md b/api/docs/descriptions/electionListByFilterHandler.md similarity index 71% rename from api/docs/descriptions/electionFilterPaginatedHandler.md rename to api/docs/descriptions/electionListByFilterHandler.md index d058edd46..63cd19fae 100644 --- a/api/docs/descriptions/electionFilterPaginatedHandler.md +++ b/api/docs/descriptions/electionListByFilterHandler.md @@ -11,4 +11,6 @@ Returns a filtered list of elections. The filters have to be sent on the request `electionId` can be partial. -See [elections list](elections-list) \ No newline at end of file +See [elections list](elections-list) + +(deprecated, in favor of /elections?page=xxx&organizationId=xxx&status=xxx) diff --git a/api/docs/models/models.go b/api/docs/models/models.go index f9455a695..b0e8bf4fe 100644 --- a/api/docs/models/models.go +++ b/api/docs/models/models.go @@ -19,37 +19,3 @@ import ( // @Success 200 {object} models.Tx_SetKeykeeper func ChainTxHandler() { } - -// ElectionListByStatusHandler -// -// Add multiple router on swagger generation has this bug https://github.com/swaggo/swag/issues/1267 -// -// @Summary List organization elections by status -// @Description List the elections of an organization by status -// @Tags Accounts -// @Accept json -// @Produce json -// @Param organizationID path string true "Specific organizationID" -// @Param status path string true "Status of the election" Enums(ready, paused, canceled, ended, results) -// @Param page path number true "Define de page number" -// @Success 200 {object} object{elections=[]api.ElectionSummary} -// @Router /accounts/{organizationID}/elections/status/{status}/page/{page} [get] -func ElectionListByStatusHandler() { -} - -// CensusPublishRootHandler -// -// Add multiple router on swagger generation has this bug https://github.com/swaggo/swag/issues/1267 -// -// @Summary Publish census at root -// @Description.markdown censusPublishHandler -// @Tags Censuses -// @Accept json -// @Produce json -// @Security BasicAuth -// @Success 200 {object} object{census=object{censusID=string,uri=string}} "It return published censusID and the ipfs uri where its uploaded" -// @Param censusID path string true "Census id" -// @Param root path string true "Specific root where to publish the census. Not required" -// @Router /censuses/{censusID}/publish/{root} [post] -func CensusPublishRootHandler() { -} diff --git a/api/elections.go b/api/elections.go index 99ca26f63..e857ed8d8 100644 --- a/api/elections.go +++ b/api/elections.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "encoding/json" "errors" - "strconv" "strings" "time" @@ -34,12 +33,20 @@ func (a *API) enableElectionHandlers() error { "/elections/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.electionFullListHandler, + a.electionListByPageHandler, ); err != nil { return err } if err := a.Endpoint.RegisterMethod( - "/elections/{electionID}", + "/elections", + "GET", + apirest.MethodAccessTypePublic, + a.electionListHandler, + ); err != nil { + return err + } + if err := a.Endpoint.RegisterMethod( + "/elections/{electionId}", "GET", apirest.MethodAccessTypePublic, a.electionHandler, @@ -47,7 +54,7 @@ func (a *API) enableElectionHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/elections/{electionID}/keys", + "/elections/{electionId}/keys", "GET", apirest.MethodAccessTypePublic, a.electionKeysHandler, @@ -55,7 +62,7 @@ func (a *API) enableElectionHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/elections/{electionID}/votes/count", + "/elections/{electionId}/votes/count", "GET", apirest.MethodAccessTypePublic, a.electionVotesCountHandler, @@ -63,15 +70,15 @@ func (a *API) enableElectionHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/elections/{electionID}/votes/page/{page}", + "/elections/{electionId}/votes/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.electionVotesHandler, + a.electionVotesListByPageHandler, ); err != nil { return err } if err := a.Endpoint.RegisterMethod( - "/elections/{electionID}/scrutiny", + "/elections/{electionId}/scrutiny", "GET", apirest.MethodAccessTypePublic, a.electionScrutinyHandler, @@ -107,7 +114,15 @@ func (a *API) enableElectionHandlers() error { "/elections/filter/page/{page}", "POST", apirest.MethodAccessTypePublic, - a.electionFilterPaginatedHandler, + a.electionListByFilterAndPageHandler, + ); err != nil { + return err + } + if err := a.Endpoint.RegisterMethod( + "/elections/filter", + "POST", + apirest.MethodAccessTypePublic, + a.electionListByFilterHandler, ); err != nil { return err } @@ -124,47 +139,162 @@ func (a *API) enableElectionHandlers() error { return nil } -// electionFullListHandler +// electionListByFilterAndPageHandler +// +// @Summary List elections (filtered) +// @Deprecated +// @Description (deprecated, in favor of /elections?page=xxx&organizationId=xxx&status=xxx) +// @Tags Elections +// @Accept json +// @Produce json +// @Param page path number true "Page" +// @Param body body ElectionParams true "Filtered by partial organizationId, partial electionId, election status and with results available or not" +// @Success 200 {object} ElectionsList +// @Router /elections/filter/page/{page} [post] +func (a *API) electionListByFilterAndPageHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { + // get params from the request body + params := &ElectionParams{} + + // but support legacy URLParam + urlParams, err := parsePaginationParams(ctx.URLParam(ParamPage), "") + if err != nil { + return err + } + params.PaginationParams = urlParams + + if err := json.Unmarshal(msg.Data, ¶ms); err != nil { + return ErrCantParseDataAsJSON.WithErr(err) + } + if params == nil { // happens when client POSTs a literal `null` JSON + return ErrMissingParameter + } + + return a.sendElectionList(ctx, params) +} + +// electionListByFilterHandler +// +// @Summary List elections (filtered) +// @Deprecated +// @Description.markdown electionListByFilterHandler +// @Tags Elections +// @Accept json +// @Produce json +// @Param page query number false "Page" +// @Param body body ElectionParams true "Filtered by partial organizationId, partial electionId, election status and with results available or not" +// @Success 200 {object} ElectionsList +// @Router /elections/filter [post] +func (a *API) electionListByFilterHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { + // get params from the request body + params := &ElectionParams{} + if err := json.Unmarshal(msg.Data, ¶ms); err != nil { + return ErrCantParseDataAsJSON.WithErr(err) + } + + return a.sendElectionList(ctx, params) +} + +// electionListByPageHandler // // @Summary List elections -// @Description Get a list of elections summaries. +// @Description Get a list of elections summaries +// @Deprecated +// @Description (deprecated, in favor of /elections?page=xxx) // @Tags Elections // @Accept json // @Produce json -// @Param page path number true "Page " -// @Success 200 {object} ElectionSummary +// @Param page path number true "Page" +// @Success 200 {object} ElectionsList // @Router /elections/page/{page} [get] -func (a *API) electionFullListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - page := 0 - if ctx.URLParam("page") != "" { - var err error - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber.With(ctx.URLParam("page")) - } +func (a *API) electionListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseElectionParams( + ctx.URLParam(ParamPage), + "", + "", + "", + "", + "", + ) + if err != nil { + return err } - elections, err := a.indexer.ProcessList(nil, page*MaxPageSize, MaxPageSize, "", 0, 0, "", false) + return a.sendElectionList(ctx, params) +} + +// electionListHandler +// +// @Summary List elections +// @Description Get a list of elections summaries. +// @Tags Elections +// @Accept json +// @Produce json +// @Param page query number false "Page" +// @Param limit query number false "Items per page" +// @Param organizationId query string false "Filter by partial organizationId" +// @Param status query string false "Election status" Enums(ready, paused, canceled, ended, results) +// @Param electionId query string false "Filter by partial electionId" +// @Param withResults query boolean false "Return only elections with published results" +// @Success 200 {object} ElectionsList +// @Router /elections [get] +func (a *API) electionListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseElectionParams( + ctx.QueryParam(ParamPage), + ctx.QueryParam(ParamLimit), + ctx.QueryParam(ParamStatus), + ctx.QueryParam(ParamOrganizationId), + ctx.QueryParam(ParamElectionId), + ctx.QueryParam(ParamWithResults), + ) if err != nil { - return ErrCantFetchElectionList.WithErr(err) + return err } + return a.sendElectionList(ctx, params) +} - list := []ElectionSummary{} - for _, eid := range elections { +// sendElectionList produces a filtered, paginated ElectionsList, +// and sends it marshalled over ctx.Send +// +// Errors returned are always of type APIerror. +func (a *API) sendElectionList(ctx *httprouter.HTTPContext, params *ElectionParams) error { + status, err := parseStatus(params.Status) + if err != nil { + return err + } + + eids, total, err := a.indexer.ProcessList( + params.OrganizationID, + params.Page*params.Limit, + params.Limit, + params.ElectionID.String(), + 0, + 0, + status, + params.WithResults, + ) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) + } + + if params.Page == 0 && total == 0 { + return ErrElectionNotFound + } + + pagination, err := calculatePagination(params.Page, params.Limit, total) + if err != nil { + return err + } + + list := &ElectionsList{ + Pagination: pagination, + } + for _, eid := range eids { e, err := a.indexer.ProcessInfo(eid) if err != nil { return ErrCantFetchElection.Withf("(%x): %v", eid, err) } - list = append(list, a.electionSummary(e)) + list.Elections = append(list.Elections, a.electionSummary(e)) } - // wrap list in a struct to consistently return list in an object, return empty - // object if the list does not contains any result - data, err := json.Marshal(struct { - Elections []ElectionSummary `json:"elections"` - }{list}) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - return ctx.Send(data, apirest.HTTPstatusOK) + return marshalAndSend(ctx, list) } // electionHandler @@ -174,13 +304,13 @@ func (a *API) electionFullListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCo // @Tags Elections // @Accept json // @Produce json -// @Param electionID path string true "Election id" +// @Param electionId path string true "Election id" // @Success 200 {object} Election -// @Router /elections/{electionID} [get] +// @Router /elections/{electionId} [get] func (a *API) electionHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("electionID"))) + electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamElectionId))) if err != nil { - return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam("electionID"), err) + return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam(ParamElectionId), err) } proc, err := a.indexer.ProcessInfo(electionID) if err != nil { @@ -245,13 +375,13 @@ func (a *API) electionHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) e // @Tags Elections // @Accept json // @Produce json -// @Param electionID path string true "Election id" -// @Success 200 {object} object{count=number} -// @Router /elections/{electionID}/votes/count [get] +// @Param electionId path string true "Election id" +// @Success 200 {object} CountResult +// @Router /elections/{electionId}/votes/count [get] func (a *API) electionVotesCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("electionID"))) + electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamElectionId))) if err != nil || electionID == nil { - return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam("electionID"), err) + return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam(ParamElectionId), err) } // check process exists and return 404 if not // TODO: use the indexer to count votes @@ -265,15 +395,7 @@ func (a *API) electionVotesCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTP } else if err != nil { return ErrCantCountVotes.WithErr(err) } - data, err := json.Marshal( - struct { - Count uint64 `json:"count"` - }{Count: count}, - ) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - return ctx.Send(data, apirest.HTTPstatusOK) + return marshalAndSend(ctx, &CountResult{Count: count}) } // electionKeysHandler @@ -283,13 +405,13 @@ func (a *API) electionVotesCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTP // @Tags Elections // @Accept json // @Produce json -// @Param electionID path string true "Election id" +// @Param electionId path string true "Election id" // @Success 200 {object} ElectionKeys -// @Router /elections/{electionID}/keys [get] +// @Router /elections/{electionId}/keys [get] func (a *API) electionKeysHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("electionID"))) + electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamElectionId))) if err != nil || electionID == nil { - return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam("electionID"), err) + return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam(ParamElectionId), err) } // TODO: sqlite also has public and private keys, consider using it instead process, err := getElection(electionID, a.vocapp.State) @@ -327,59 +449,29 @@ func (a *API) electionKeysHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContex return ctx.Send(data, apirest.HTTPstatusOK) } -// electionVotesHandler +// electionVotesListByPageHandler // // @Summary List election votes // @Description Returns the list of voteIDs for an election (paginated) +// @Deprecated +// @Description (deprecated, in favor of /votes?page=xxx&electionId=xxx) // @Tags Elections // @Accept json // @Produce json -// @Param electionID path string true "Election id" -// @Param page path number true "Page " -// @Success 200 {object} Vote -// @Router /elections/{electionID}/votes/page/{page} [get] -func (a *API) electionVotesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("electionID"))) - if err != nil || electionID == nil { - return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam("electionID"), err) - } - // TODO: remove the getElection call? - if _, err := getElection(electionID, a.vocapp.State); err != nil { - return err - } - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber - } - } - page = page * MaxPageSize - - votesRaw, err := a.indexer.GetEnvelopes(electionID, MaxPageSize, page, "") - if err != nil { - if errors.Is(err, indexer.ErrVoteNotFound) { - return ErrVoteNotFound - } - return ErrCantFetchEnvelope.WithErr(err) - } - votes := []Vote{} - for _, v := range votesRaw { - votes = append(votes, Vote{ - VoteID: v.Nullifier, - VoterID: v.VoterID, - TxHash: v.TxHash, - BlockHeight: v.Height, - TransactionIndex: &v.TxIndex, - }) - } - data, err := json.Marshal(struct { - Votes []Vote `json:"votes"` - }{votes}) +// @Param electionId path string true "Election id" +// @Param page path number true "Page" +// @Success 200 {object} VotesList +// @Router /elections/{electionId}/votes/page/{page} [get] +func (a *API) electionVotesListByPageHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseVoteParams( + ctx.URLParam(ParamPage), + "", + ctx.URLParam(ParamElectionId), + ) if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) + return err } - return ctx.Send(data, apirest.HTTPstatusOK) + return a.sendVotesList(ctx, params) } // electionScrutinyHandler @@ -389,13 +481,13 @@ func (a *API) electionVotesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPConte // @Tags Elections // @Accept json // @Produce json -// @Param electionID path string true "Election id" +// @Param electionId path string true "Election id" // @Success 200 {object} ElectionResults -// @Router /elections/{electionID}/scrutiny [get] +// @Router /elections/{electionId}/scrutiny [get] func (a *API) electionScrutinyHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("electionID"))) + electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamElectionId))) if err != nil || electionID == nil { - return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam("electionID"), err) + return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam(ParamElectionId), err) } process, err := getElection(electionID, a.vocapp.State) if err != nil { @@ -612,78 +704,6 @@ func getElection(electionID []byte, vs *state.State) (*models.Process, error) { return process, nil } -// electionFilterPaginatedHandler -// -// @Summary List elections (filtered) -// @Description.markdown electionFilterPaginatedHandler -// @Tags Elections -// @Accept json -// @Produce json -// @Param page path number true "Page to paginate" -// @Param transaction body ElectionFilter true "Filtered by partial organizationID, partial processID, process status and with results available or not" -// @Success 200 {object} ElectionSummary -// @Router /elections/filter/page/{page} [post] -func (a *API) electionFilterPaginatedHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { - // get organizationId from the request body - body := &ElectionFilter{} - if err := json.Unmarshal(msg.Data, &body); err != nil { - return ErrCantParseDataAsJSON.WithErr(err) - } - // check that at least one filter is set - if body.OrganizationID == nil && body.ElectionID == nil && body.Status == "" && body.WithResults == nil { - return ErrMissingParameter - } - // get page - var err error - page := 0 - if ctx.URLParam("page") != "" { - page, err = strconv.Atoi(ctx.URLParam("page")) - if err != nil { - return ErrCantParsePageNumber.WithErr(err) - } - } - page = page * MaxPageSize - if body.WithResults == nil { - withResults := false - body.WithResults = &withResults - } - elections, err := a.indexer.ProcessList( - body.OrganizationID, - page, - MaxPageSize, - body.ElectionID.String(), - 0, - 0, - body.Status, - *body.WithResults, - ) - if err != nil { - return ErrCantFetchElectionList.WithErr(err) - } - if len(elections) == 0 { - return ErrElectionNotFound - } - - var list []ElectionSummary - // get election summary - for _, eid := range elections { - e, err := a.indexer.ProcessInfo(eid) - if err != nil { - return ErrCantFetchElection.WithErr(err) - } - list = append(list, a.electionSummary(e)) - } - data, err := json.Marshal(struct { - Elections []ElectionSummary `json:"elections"` - }{ - Elections: list, - }) - if err != nil { - return ErrMarshalingServerJSONFailed.WithErr(err) - } - return ctx.Send(data, apirest.HTTPstatusOK) -} - // buildElectionIDHandler // // @Summary Build an election ID @@ -691,7 +711,7 @@ func (a *API) electionFilterPaginatedHandler(msg *apirest.APIdata, ctx *httprout // @Tags Elections // @Accept json // @Produce json -// @Param transaction body BuildElectionID true "Delta, OrganizationID, CensusOrigin and EnvelopeType" +// @Param transaction body BuildElectionID true "delta, organizationId, censusOrigin and envelopeType" // @Success 200 {object} object{electionID=string} // @Router /elections/id [post] func (a *API) buildElectionIDHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContext) error { @@ -725,3 +745,36 @@ func (a *API) buildElectionIDHandler(msg *apirest.APIdata, ctx *httprouter.HTTPC } return ctx.Send(data, apirest.HTTPstatusOK) } + +// parseElectionParams returns an ElectionParams filled with the passed params +func parseElectionParams(paramPage, paramLimit, paramStatus, + paramOrganizationID, paramElectionID, paramWithResults string, +) (*ElectionParams, error) { + pagination, err := parsePaginationParams(paramPage, paramLimit) + if err != nil { + return nil, err + } + + organizationID, err := parseHexString(paramOrganizationID) + if err != nil { + return nil, err + } + + electionID, err := parseHexString(paramElectionID) + if err != nil { + return nil, err + } + + withResults, err := parseBool(paramWithResults) + if err != nil { + return nil, err + } + + return &ElectionParams{ + PaginationParams: pagination, + OrganizationID: organizationID, + ElectionID: electionID, + WithResults: withResults, + Status: paramStatus, + }, nil +} diff --git a/api/errors.go b/api/errors.go index 6346b0017..f4df72b7c 100644 --- a/api/errors.go +++ b/api/errors.go @@ -38,25 +38,25 @@ var ( ErrMetadataURINotMatchContent = apirest.APIerror{Code: 4010, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("metadata URI does not match metadata content")} ErrMarshalingJSONFailed = apirest.APIerror{Code: 4011, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("marshaling JSON failed")} ErrFileSizeTooBig = apirest.APIerror{Code: 4012, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("file size exceeds the maximum allowed")} - ErrCantParseOrgID = apirest.APIerror{Code: 4013, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse organizationID")} - ErrCantParseAccountID = apirest.APIerror{Code: 4014, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse accountID")} + ErrCantParseOrgID = apirest.APIerror{Code: 4013, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse organizationId")} + ErrCantParseAccountID = apirest.APIerror{Code: 4014, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse accountId")} ErrCantParseBearerToken = apirest.APIerror{Code: 4015, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse bearer token")} ErrCantParseDataAsJSON = apirest.APIerror{Code: 4016, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse data as JSON")} - ErrCantParseElectionID = apirest.APIerror{Code: 4017, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse electionID")} + ErrCantParseElectionID = apirest.APIerror{Code: 4017, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse electionId")} ErrCantParseMetadataAsJSON = apirest.APIerror{Code: 4018, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse metadata (invalid format)")} - ErrCantParsePageNumber = apirest.APIerror{Code: 4019, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse page number")} + ErrCantParseNumber = apirest.APIerror{Code: 4019, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse number")} ErrCantParsePayloadAsJSON = apirest.APIerror{Code: 4020, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse payload as JSON")} - ErrCantParseVoteID = apirest.APIerror{Code: 4021, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse voteID")} + ErrCantParseVoteID = apirest.APIerror{Code: 4021, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse voteId")} ErrCantExtractMetadataURI = apirest.APIerror{Code: 4022, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot extract metadata URI")} - ErrVoteIDMalformed = apirest.APIerror{Code: 4023, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("voteID is malformed")} + ErrVoteIDMalformed = apirest.APIerror{Code: 4023, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("voteId is malformed")} ErrVoteNotFound = apirest.APIerror{Code: 4024, HTTPstatus: apirest.HTTPstatusNotFound, Err: fmt.Errorf("vote not found")} - ErrCensusIDLengthInvalid = apirest.APIerror{Code: 4025, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("censusID length is wrong")} + ErrCensusIDLengthInvalid = apirest.APIerror{Code: 4025, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("censusId length is wrong")} ErrCensusRootIsNil = apirest.APIerror{Code: 4026, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("census root is nil")} ErrCensusTypeUnknown = apirest.APIerror{Code: 4027, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("census type is unknown")} ErrCensusTypeMismatch = apirest.APIerror{Code: 4028, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("census type mismatch")} ErrCensusIndexedFlagMismatch = apirest.APIerror{Code: 4029, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("census indexed flag mismatch")} ErrCensusRootHashMismatch = apirest.APIerror{Code: 4030, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("census root hash mismatch after importing dump")} - ErrParamStatusMissing = apirest.APIerror{Code: 4031, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("parameter (status) missing or invalid")} + ErrParamStatusInvalid = apirest.APIerror{Code: 4031, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("parameter (status) invalid")} ErrParamParticipantsMissing = apirest.APIerror{Code: 4032, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("parameter (participants) missing")} ErrParamParticipantsTooBig = apirest.APIerror{Code: 4033, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("parameter (participants) exceeds max length per call")} ErrParamDumpOrRootMissing = apirest.APIerror{Code: 4034, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("parameter (dump or root) missing")} @@ -80,16 +80,17 @@ var ( ErrUnmarshalingServerProto = apirest.APIerror{Code: 4052, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("error unmarshaling protobuf data")} ErrMarshalingServerProto = apirest.APIerror{Code: 4053, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("error marshaling protobuf data")} ErrSIKNotFound = apirest.APIerror{Code: 4054, HTTPstatus: apirest.HTTPstatusNotFound, Err: fmt.Errorf("SIK not found")} + ErrCantParseBoolean = apirest.APIerror{Code: 4055, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse string into boolean")} + ErrCantParseHexString = apirest.APIerror{Code: 4056, HTTPstatus: apirest.HTTPstatusBadRequest, Err: fmt.Errorf("cannot parse string into hex bytes")} + ErrPageNotFound = apirest.APIerror{Code: 4057, HTTPstatus: apirest.HTTPstatusNotFound, Err: fmt.Errorf("page not found")} ErrVochainEmptyReply = apirest.APIerror{Code: 5000, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain returned an empty reply")} ErrVochainSendTxFailed = apirest.APIerror{Code: 5001, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain SendTx failed")} ErrVochainGetTxFailed = apirest.APIerror{Code: 5002, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain GetTx failed")} ErrVochainReturnedErrorCode = apirest.APIerror{Code: 5003, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain replied with error code")} - ErrVochainReturnedInvalidElectionID = apirest.APIerror{Code: 5004, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain returned an invalid electionID after executing tx")} + ErrVochainReturnedInvalidElectionID = apirest.APIerror{Code: 5004, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain returned an invalid electionId after executing tx")} ErrVochainReturnedWrongMetadataCID = apirest.APIerror{Code: 5005, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("vochain returned an unexpected metadata CID after executing tx")} ErrMarshalingServerJSONFailed = apirest.APIerror{Code: 5006, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("marshaling (server-side) JSON failed")} - ErrCantFetchElectionList = apirest.APIerror{Code: 5007, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("cannot fetch election list")} ErrCantFetchElection = apirest.APIerror{Code: 5008, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("cannot fetch election")} - ErrCantFetchElectionResults = apirest.APIerror{Code: 5009, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("cannot fetch election results")} // unused as of 2023-06-28 ErrCantFetchTokenTransfers = apirest.APIerror{Code: 5010, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("cannot fetch token transfers")} ErrCantFetchEnvelopeHeight = apirest.APIerror{Code: 5011, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("cannot fetch envelope height")} ErrCantFetchEnvelope = apirest.APIerror{Code: 5012, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("cannot fetch vote envelope")} @@ -113,4 +114,5 @@ var ( ErrVochainOverloaded = apirest.APIerror{Code: 5030, HTTPstatus: apirest.HTTPstatusServiceUnavailable, Err: fmt.Errorf("vochain overloaded")} ErrGettingSIK = apirest.APIerror{Code: 5031, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("error getting SIK")} ErrCensusBuild = apirest.APIerror{Code: 5032, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("error building census")} + ErrIndexerQueryFailed = apirest.APIerror{Code: 5033, HTTPstatus: apirest.HTTPstatusInternalErr, Err: fmt.Errorf("indexer query failed")} ) diff --git a/api/helpers.go b/api/helpers.go index 813ba926e..8c084ebf7 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -5,7 +5,10 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/big" + "strconv" + "strings" cometpool "github.com/cometbft/cometbft/mempool" cometcoretypes "github.com/cometbft/cometbft/rpc/core/types" @@ -13,7 +16,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/iancoleman/strcase" "go.vocdoni.io/dvote/crypto/nacl" + "go.vocdoni.io/dvote/httprouter" + "go.vocdoni.io/dvote/httprouter/apirest" "go.vocdoni.io/dvote/types" + "go.vocdoni.io/dvote/util" "go.vocdoni.io/dvote/vochain/indexer/indexertypes" "go.vocdoni.io/proto/build/go/models" "google.golang.org/protobuf/encoding/protojson" @@ -162,3 +168,157 @@ func decryptVotePackage(vp []byte, privKeys []string, indexes []uint32) ([]byte, } return vp, nil } + +// marshalAndSend marshals any passed struct and sends it over ctx.Send() +func marshalAndSend(ctx *httprouter.HTTPContext, v any) error { + data, err := json.Marshal(v) + if err != nil { + return ErrMarshalingServerJSONFailed.WithErr(err) + } + return ctx.Send(data, apirest.HTTPstatusOK) +} + +// parseNumber parses a string into an int. +// +// If the string is not parseable, returns an APIerror. +// +// The empty string "" is treated specially, returns 0 with no error. +func parseNumber(s string) (int, error) { + if s == "" { + return 0, nil + } + page, err := strconv.Atoi(s) + if err != nil { + return 0, ErrCantParseNumber.With(s) + } + return page, nil +} + +// parsePage parses a string into an int. +// +// If the resulting int is negative, returns ErrNoSuchPage. +// If the string is not parseable, returns an APIerror. +// +// The empty string "" is treated specially, returns 0 with no error. +func parsePage(s string) (int, error) { + page, err := parseNumber(s) + if err != nil { + return 0, err + } + if page < 0 { + return 0, ErrPageNotFound + } + return page, nil +} + +// parseLimit parses a string into an int. +// +// The empty string "" is treated specially, returns DefaultItemsPerPage with no error. +// If the resulting int is higher than MaxItemsPerPage, returns MaxItemsPerPage. +// If the resulting int is 0 or negative, returns DefaultItemsPerPage. +// +// If the string is not parseable, returns an APIerror. +func parseLimit(s string) (int, error) { + limit, err := parseNumber(s) + if err != nil { + return 0, err + } + if limit > MaxItemsPerPage { + limit = MaxItemsPerPage + } + if limit <= 0 { + limit = DefaultItemsPerPage + } + return limit, nil +} + +// parseStatus converts a string ("READY", "ready", "PAUSED", etc) +// to a models.ProcessStatus. +// +// If the string doesn't map to a value, returns an APIerror. +// +// The empty string "" is treated specially, returns 0 with no error. +func parseStatus(s string) (models.ProcessStatus, error) { + if s == "" { + return 0, nil + } + status, found := models.ProcessStatus_value[strings.ToUpper(s)] + if !found { + return 0, ErrParamStatusInvalid.With(s) + } + return models.ProcessStatus(status), nil +} + +// parseHexString converts a string like 0x1234cafe (or 1234cafe) +// to a types.HexBytes. +// +// If the string can't be parsed, returns an APIerror. +func parseHexString(s string) (types.HexBytes, error) { + orgID, err := hex.DecodeString(util.TrimHex(s)) + if err != nil { + return nil, ErrCantParseHexString.Withf("%q", s) + } + return orgID, nil +} + +// parseBool parses a string into a boolean value. +// +// The empty string "" is treated specially, returns false with no error. +func parseBool(s string) (bool, error) { + if s == "" { + return false, nil + } + b, err := strconv.ParseBool(s) + if err != nil { + return false, ErrCantParseBoolean.With(s) + } + return b, nil +} + +// parsePaginationParams returns a PaginationParams filled with the passed params +func parsePaginationParams(paramPage, paramLimit string) (PaginationParams, error) { + page, err := parsePage(paramPage) + if err != nil { + return PaginationParams{}, err + } + + limit, err := parseLimit(paramLimit) + if err != nil { + return PaginationParams{}, err + } + + return PaginationParams{ + Page: page, + Limit: limit, + }, nil +} + +// calculatePagination calculates PreviousPage, NextPage and LastPage. +// +// If page is negative or higher than LastPage, returns an APIerror (ErrPageNotFound) +func calculatePagination(page int, limit int, totalItems uint64) (*Pagination, error) { + // pages start at 0 index, for legacy reasons + lastp := int(math.Ceil(float64(totalItems)/float64(limit)) - 1) + + if page > lastp || page < 0 { + return nil, ErrPageNotFound + } + + var prevp, nextp *uint64 + if page > 0 { + prevPage := uint64(page - 1) + prevp = &prevPage + } + if page < lastp { + nextPage := uint64(page + 1) + nextp = &nextPage + } + + return &Pagination{ + TotalItems: totalItems, + PreviousPage: prevp, + CurrentPage: uint64(page), + NextPage: nextp, + LastPage: uint64(lastp), + }, nil +} diff --git a/api/vote.go b/api/vote.go index 9cd774a04..8372797c5 100644 --- a/api/vote.go +++ b/api/vote.go @@ -26,7 +26,15 @@ func (a *API) enableVoteHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/votes/{voteID}", + "/votes", + "GET", + apirest.MethodAccessTypePublic, + a.votesListHandler, + ); err != nil { + return err + } + if err := a.Endpoint.RegisterMethod( + "/votes/{voteId}", "GET", apirest.MethodAccessTypePublic, a.getVoteHandler, @@ -34,7 +42,7 @@ func (a *API) enableVoteHandlers() error { return err } if err := a.Endpoint.RegisterMethod( - "/votes/verify/{electionID}/{voteID}", + "/votes/verify/{electionId}/{voteId}", "GET", apirest.MethodAccessTypePublic, a.verifyVoteHandler, @@ -90,9 +98,9 @@ func (a *API) submitVoteHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContex // @Produce json // @Param voteID path string true "Nullifier of the vote" // @Success 200 {object} Vote -// @Router /votes/{voteID} [get] +// @Router /votes/{voteId} [get] func (a *API) getVoteHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - voteID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("voteID"))) + voteID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamVoteId))) if err != nil { return ErrCantParseVoteID.WithErr(err) } @@ -162,22 +170,22 @@ func (a *API) getVoteHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) er // @Tags Votes // @Accept json // @Produce json -// @Param electionID path string true "Election id" -// @Param voteID path string true "Nullifier of the vote" +// @Param electionId path string true "Election id" +// @Param voteId path string true "Nullifier of the vote" // @Success 200 "(empty body)" // @Failure 404 {object} apirest.APIerror -// @Router /votes/verify/{electionID}/{voteID} [get] +// @Router /votes/verify/{electionId}/{voteId} [get] func (a *API) verifyVoteHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - voteID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("voteID"))) + voteID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamVoteId))) if err != nil { return ErrCantParseVoteID.WithErr(err) } if len(voteID) == 0 { return ErrVoteIDMalformed.Withf("%x", voteID) } - electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("electionID"))) + electionID, err := hex.DecodeString(util.TrimHex(ctx.URLParam(ParamElectionId))) if err != nil { - return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam("electionID"), err) + return ErrCantParseElectionID.Withf("(%s): %v", ctx.URLParam(ParamElectionId), err) } if len(voteID) != types.ProcessIDsize { return ErrVoteIDMalformed.Withf("%x", voteID) @@ -188,3 +196,84 @@ func (a *API) verifyVoteHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) } return ctx.Send(nil, apirest.HTTPstatusOK) } + +// votesListHandler +// +// @Summary List votes +// @Description Returns the list of votes +// @Tags Votes +// @Accept json +// @Produce json +// @Param page query number false "Page" +// @Param limit query number false "Items per page" +// @Param electionId query string false "Election id" +// @Success 200 {object} VotesList +// @Router /votes [get] +func (a *API) votesListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + params, err := parseVoteParams( + ctx.QueryParam(ParamPage), + ctx.QueryParam(ParamLimit), + ctx.QueryParam(ParamElectionId), + ) + if err != nil { + return err + } + return a.sendVotesList(ctx, params) +} + +// sendVotesList produces a filtered, paginated VotesList, +// and sends it marshalled over ctx.Send +// +// Errors returned are always of type APIerror. +func (a *API) sendVotesList(ctx *httprouter.HTTPContext, params *VoteParams) error { + votes, total, err := a.indexer.VoteList( + params.ElectionID, + params.Limit, + params.Page*params.Limit, + "", + ) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) + } + + if params.Page == 0 && total == 0 { + return ErrVoteNotFound + } + + pagination, err := calculatePagination(params.Page, params.Limit, total) + if err != nil { + return err + } + + list := &VotesList{ + Pagination: pagination, + } + for _, vote := range votes { + list.Votes = append(list.Votes, Vote{ + VoteID: vote.Nullifier, + VoterID: vote.VoterID, + TxHash: vote.TxHash, + BlockHeight: vote.Height, + TransactionIndex: &vote.TxIndex, + }) + } + return marshalAndSend(ctx, list) +} + +// parseVoteParams returns an VoteParams filled with the passed params +func parseVoteParams(paramPage, paramLimit, paramElectionId string) (*VoteParams, error) { + pagination, err := parsePaginationParams(paramPage, paramLimit) + if err != nil { + return nil, err + } + + electionId, err := parseHexString(paramElectionId) + if err != nil { + return nil, err + } + + return &VoteParams{ + PaginationParams: pagination, + ElectionID: electionId, + }, nil +} diff --git a/httprouter/message.go b/httprouter/message.go index c17a12fcd..b1723dd29 100644 --- a/httprouter/message.go +++ b/httprouter/message.go @@ -44,6 +44,12 @@ func (h *HTTPContext) URLParam(key string) string { return chi.URLParam(h.Request, key) } +// QueryParam is a wrapper around go-chi to get the value of a query string parameter (like "?key=value"). +// If key is not present, returns the empty string. +func (h *HTTPContext) QueryParam(key string) string { + return h.Request.URL.Query().Get(key) +} + // Send replies the request with the provided message. func (h *HTTPContext) Send(msg []byte, httpStatusCode int) error { defer func() { diff --git a/test/api_test.go b/test/api_test.go index 3308d1be8..a4ccec4c5 100644 --- a/test/api_test.go +++ b/test/api_test.go @@ -87,7 +87,7 @@ func TestAPIcensusAndVote(t *testing.T) { qt.Assert(t, censusData.Weight.String(), qt.Equals, "1") electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100} - election := createElection(t, c, server.Account, electionParams, censusData.CensusRoot, 0, server.VochainAPP.ChainID(), false) + election := createElection(t, c, server.Account, electionParams, censusData.CensusRoot, 0, server.VochainAPP.ChainID(), false, 0) // Block 2 server.VochainAPP.AdvanceTestBlock() @@ -216,6 +216,63 @@ func TestAPIaccount(t *testing.T) { qt.Assert(t, gotAcct.Balance, qt.Equals, initBalance) } +func TestAPIAccountsList(t *testing.T) { + server := testcommon.APIserver{} + server.Start(t, + api.ChainHandler, + api.CensusHandler, + api.VoteHandler, + api.AccountHandler, + api.ElectionHandler, + api.WalletHandler, + ) + token1 := uuid.New() + c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1) + + // Block 1 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 1) + + // create new accounts + for nonce := uint32(0); nonce < 20; nonce++ { + createAccount(t, c, server, uint64(80)) + } + + // Block 2 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 2) + + // Get the list and check it + fetchAL := func(method string, jsonBody any, query string, urlPath ...string) api.AccountsList { + resp, code := c.RequestWithQuery(method, jsonBody, query, urlPath...) + list := api.AccountsList{} + qt.Assert(t, code, qt.Equals, 200) + err := json.Unmarshal(resp, &list) + qt.Assert(t, err, qt.IsNil) + return list + } + + el := make(map[string]api.AccountsList) + el["0"] = fetchAL("GET", nil, "", "accounts") + el["1"] = fetchAL("GET", nil, "page=1", "accounts") + el["p0"] = fetchAL("GET", nil, "", "accounts", "page", "0") + el["p1"] = fetchAL("GET", nil, "", "accounts", "page", "1") + + qt.Assert(t, el["0"], qt.Not(qt.DeepEquals), el["1"]) + qt.Assert(t, el["0"], qt.DeepEquals, el["p0"]) + qt.Assert(t, el["1"], qt.DeepEquals, el["p1"]) + + // 2 accounts pre-exist: the faucet account, and the burn address + qt.Assert(t, el["0"].Pagination.TotalItems, qt.Equals, uint64(2+20)) + qt.Assert(t, el["1"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + qt.Assert(t, el["p0"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + qt.Assert(t, el["p1"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + + for _, item := range el { + qt.Assert(t, len(item.Accounts), qt.Equals, api.DefaultItemsPerPage) + } +} + func TestAPIElectionCost(t *testing.T) { // cheap election runAPIElectionCostWithParams(t, @@ -394,6 +451,41 @@ func TestAPIAccountTokentxs(t *testing.T) { qt.Assert(t, gotAcct1.Balance, qt.Equals, initBalance+amountAcc2toAcct1-amountAcc1toAcct2-uint64(txBasePrice)) } +func TestAPIBlocks(t *testing.T) { + server := testcommon.APIserver{} + server.Start(t, + api.ChainHandler, + api.CensusHandler, + api.VoteHandler, + api.AccountHandler, + api.ElectionHandler, + api.WalletHandler, + ) + token1 := uuid.New() + c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1) + + // Block 1 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 1) + + // create a new account + initBalance := uint64(80) + _ = createAccount(t, c, server, initBalance) + + // Block 2 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 2) + + // check the txCount + resp, code := c.Request("GET", nil, "chain", "blocks", "1") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + block := api.Block{} + err := json.Unmarshal(resp, &block) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, block.TxCount, qt.Equals, int64(1)) +} + func runAPIElectionCostWithParams(t *testing.T, electionParams electionprice.ElectionParameters, startBlock uint32, initialBalance, @@ -442,7 +534,7 @@ func runAPIElectionCostWithParams(t *testing.T, qt.Assert(t, requestAccount(t, c, signer.Address().String()).Balance, qt.Equals, initialBalance) - createElection(t, c, signer, electionParams, censusRoot, startBlock, server.VochainAPP.ChainID(), false) + createElection(t, c, signer, electionParams, censusRoot, startBlock, server.VochainAPP.ChainID(), false, 0) // Block 3 server.VochainAPP.AdvanceTestBlock() @@ -508,6 +600,7 @@ func createElection(t testing.TB, c *testutil.TestHTTPclient, startBlock uint32, chainID string, encryptedMetadata bool, + nonce uint32, ) api.ElectionCreate { metadataBytes, err := json.Marshal( &api.ElectionMetadata{ @@ -530,7 +623,7 @@ func createElection(t testing.TB, c *testutil.TestHTTPclient, tx := models.Tx_NewProcess{ NewProcess: &models.NewProcessTx{ Txtype: models.TxType_NEW_PROCESS, - Nonce: 0, + Nonce: nonce, Process: &models.Process{ StartBlock: startBlock, BlockCount: electionParams.ElectionDuration, @@ -737,7 +830,7 @@ func TestAPIBuildElectionID(t *testing.T) { // create a new election electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100} - response := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID(), false) + response := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID(), false, 0) // Block 4 server.VochainAPP.AdvanceTestBlock() @@ -807,7 +900,7 @@ func TestAPIEncryptedMetadata(t *testing.T) { // create a new election electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100} - electionResponse := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID(), true) + electionResponse := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID(), true, 0) // Block 4 server.VochainAPP.AdvanceTestBlock() @@ -833,3 +926,70 @@ func TestAPIEncryptedMetadata(t *testing.T) { qt.Assert(t, err, qt.IsNil) qt.Assert(t, metadata.Title["default"], qt.Equals, "test election") } + +func TestAPIElectionsList(t *testing.T) { + server := testcommon.APIserver{} + server.Start(t, + api.ChainHandler, + api.CensusHandler, + api.VoteHandler, + api.AccountHandler, + api.ElectionHandler, + api.WalletHandler, + ) + // Block 1 + server.VochainAPP.AdvanceTestBlock() + + token1 := uuid.New() + c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1) + + // create a new census + resp, code := c.Request("POST", nil, "censuses", "weighted") + qt.Assert(t, code, qt.Equals, 200) + censusData := &api.Census{} + qt.Assert(t, json.Unmarshal(resp, censusData), qt.IsNil) + + electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100} + for nonce := uint32(0); nonce < 20; nonce++ { + createElection(t, c, server.Account, electionParams, censusData.CensusRoot, 0, server.VochainAPP.ChainID(), false, nonce) + } + + // Block 2 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 2) + + // Get the list of elections and check it + fetchEL := func(method string, jsonBody any, query string, urlPath ...string) api.ElectionsList { + resp, code := c.RequestWithQuery(method, jsonBody, query, urlPath...) + elections := api.ElectionsList{} + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("resp: %q", resp)) + err := json.Unmarshal(resp, &elections) + qt.Assert(t, err, qt.IsNil) + return elections + } + + el := make(map[string]api.ElectionsList) + el["0"] = fetchEL("GET", nil, "", "elections") + el["1"] = fetchEL("GET", nil, "page=1", "elections") + el["p0"] = fetchEL("GET", nil, "", "elections", "page", "0") + el["p1"] = fetchEL("GET", nil, "", "elections", "page", "1") + + body := api.ElectionParams{WithResults: false} + el["fp0"] = fetchEL("POST", body, "", "elections", "filter", "page", "0") + el["fp1"] = fetchEL("POST", body, "", "elections", "filter", "page", "1") + + qt.Assert(t, el["0"], qt.Not(qt.DeepEquals), el["1"]) + qt.Assert(t, el["0"], qt.DeepEquals, el["p0"]) + qt.Assert(t, el["1"], qt.DeepEquals, el["p1"]) + + qt.Assert(t, el["0"].Pagination.TotalItems, qt.Equals, uint64(20)) + qt.Assert(t, el["1"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + qt.Assert(t, el["p0"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + qt.Assert(t, el["p1"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + qt.Assert(t, el["fp0"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + qt.Assert(t, el["fp1"].Pagination.TotalItems, qt.Equals, el["0"].Pagination.TotalItems) + + for _, item := range el { + qt.Assert(t, len(item.Elections), qt.Equals, api.DefaultItemsPerPage) + } +} diff --git a/test/apierror_test.go b/test/apierror_test.go index 6cb688b5f..5d1409db8 100644 --- a/test/apierror_test.go +++ b/test/apierror_test.go @@ -43,6 +43,10 @@ func TestAPIerror(t *testing.T) { args args want apirest.APIerror }{ + { + args: args{"GET", nil, []string{"accounts"}}, + want: api.ErrAccountNotFound, + }, { args: args{"GET", nil, []string{"accounts", "0123456789"}}, want: api.ErrAddressMalformed, @@ -65,7 +69,7 @@ func TestAPIerror(t *testing.T) { }, { args: args{"GET", nil, []string{"accounts", "totallyWrong!@#$", "elections", "status", "ready", "page", "0"}}, - want: api.ErrCantParseOrgID, + want: api.ErrCantParseHexString, }, { args: args{"GET", nil, []string{"accounts", "totallyWrong!@#$", "transfers", "page", "0"}}, @@ -110,11 +114,19 @@ func TestAPIerror(t *testing.T) { "status", "ready", "page", "-1", }}, - want: api.ErrCantFetchElectionList, + want: api.ErrPageNotFound, }, { args: args{"GET", nil, []string{"elections", "page", "thisIsTotallyNotAnInt"}}, - want: api.ErrCantParsePageNumber, + want: api.ErrCantParseNumber, + }, + { + args: args{"GET", nil, []string{"elections", "page", "1"}}, + want: api.ErrPageNotFound, + }, + { + args: args{"GET", nil, []string{"elections", "page", "-1"}}, + want: api.ErrPageNotFound, }, } for _, tt := range tests { diff --git a/test/testcommon/testutil/apiclient.go b/test/testcommon/testutil/apiclient.go index d7ae50a2f..7a560dbf7 100644 --- a/test/testcommon/testutil/apiclient.go +++ b/test/testcommon/testutil/apiclient.go @@ -21,16 +21,27 @@ type TestHTTPclient struct { t testing.TB } -func (c *TestHTTPclient) Request(method string, jsonBody any, urlPath ...string) ([]byte, int) { - body, err := json.Marshal(jsonBody) +func (c *TestHTTPclient) RequestWithQuery(method string, jsonBody any, query string, urlPath ...string) ([]byte, int) { + u, err := url.Parse(c.addr.String()) qt.Assert(c.t, err, qt.IsNil) + u.RawQuery = query + return c.request(method, u, jsonBody, urlPath...) +} + +func (c *TestHTTPclient) Request(method string, jsonBody any, urlPath ...string) ([]byte, int) { u, err := url.Parse(c.addr.String()) qt.Assert(c.t, err, qt.IsNil) + return c.request(method, u, jsonBody, urlPath...) +} + +func (c *TestHTTPclient) request(method string, u *url.URL, jsonBody any, urlPath ...string) ([]byte, int) { u.Path = path.Join(u.Path, path.Join(urlPath...)) headers := http.Header{} if c.token != nil { headers = http.Header{"Authorization": []string{"Bearer " + c.token.String()}} } + body, err := json.Marshal(jsonBody) + qt.Assert(c.t, err, qt.IsNil) c.t.Logf("querying %s", u) resp, err := c.c.Do(&http.Request{ Method: method, diff --git a/vochain/app.go b/vochain/app.go index 7e0107537..f6691be61 100644 --- a/vochain/app.go +++ b/vochain/app.go @@ -290,10 +290,6 @@ func (app *BaseApplication) beginBlock(t time.Time, height uint32) { app.State.SetHeight(height) go app.State.CachePurge(height) - app.State.OnBeginBlock(vstate.BeginBlock{ - Height: int64(height), - Time: t, - }) } // endBlock is called at the end of every block. diff --git a/vochain/appsetup.go b/vochain/appsetup.go index fb52f66ed..6726b8b21 100644 --- a/vochain/appsetup.go +++ b/vochain/appsetup.go @@ -25,9 +25,6 @@ func (app *BaseApplication) SetNode(vochaincfg *config.VochainCfg) error { if app.Node, err = newTendermint(app, vochaincfg); err != nil { return fmt.Errorf("could not set tendermint node service: %s", err) } - if vochaincfg.IsSeedNode { - return nil - } // Note that cometcli.New logs any error rather than returning it. app.NodeClient = cometcli.New(app.Node) return nil diff --git a/vochain/indexer/bench_test.go b/vochain/indexer/bench_test.go index 631e83205..e47a77c63 100644 --- a/vochain/indexer/bench_test.go +++ b/vochain/indexer/bench_test.go @@ -85,6 +85,7 @@ func BenchmarkIndexer(b *testing.B) { tx := &vochaintx.Tx{ TxID: rnd.Random32(), TxModelType: "vote", + Tx: &models.Tx{Payload: &models.Tx_Vote{}}, } idx.OnNewTx(tx, height, txBlockIndex) curTxs = append(curTxs, tx) @@ -138,7 +139,11 @@ func BenchmarkFetchTx(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < numTxs; j++ { - idx.OnNewTx(&vochaintx.Tx{TxID: util.Random32()}, uint32(i), int32(j)) + idx.OnNewTx(&vochaintx.Tx{ + TxID: util.Random32(), + TxModelType: "vote", + Tx: &models.Tx{Payload: &models.Tx_Vote{}}, + }, uint32(i), int32(j)) } err := idx.Commit(uint32(i)) qt.Assert(b, err, qt.IsNil) diff --git a/vochain/indexer/block.go b/vochain/indexer/block.go index e40a5fdfb..1e817aa53 100644 --- a/vochain/indexer/block.go +++ b/vochain/indexer/block.go @@ -7,35 +7,41 @@ import ( "fmt" "time" - "go.vocdoni.io/dvote/log" - indexerdb "go.vocdoni.io/dvote/vochain/indexer/db" - "go.vocdoni.io/dvote/vochain/state" + "go.vocdoni.io/dvote/vochain/indexer/indexertypes" ) // ErrBlockNotFound is returned if the block is not found in the indexer database. var ErrBlockNotFound = fmt.Errorf("block not found") -func (idx *Indexer) OnBeginBlock(bb state.BeginBlock) { - idx.blockMu.Lock() - defer idx.blockMu.Unlock() - queries := idx.blockTxQueries() - if _, err := queries.CreateBlock(context.TODO(), indexerdb.CreateBlockParams{ - Height: bb.Height, - Time: bb.Time, - DataHash: nonNullBytes(bb.DataHash), - }); err != nil { - log.Errorw(err, "cannot index new block") +// BlockTimestamp returns the timestamp of the block at the given height +func (idx *Indexer) BlockTimestamp(height int64) (time.Time, error) { + block, err := idx.BlockByHeight(height) + if err != nil { + return time.Time{}, err } + return block.Time, nil } -// BlockTimestamp returns the timestamp of the block at the given height -func (idx *Indexer) BlockTimestamp(height int64) (time.Time, error) { - block, err := idx.readOnlyQuery.GetBlock(context.TODO(), height) +// BlockByHeight returns the available information of the block at the given height +func (idx *Indexer) BlockByHeight(height int64) (*indexertypes.Block, error) { + block, err := idx.readOnlyQuery.GetBlockByHeight(context.TODO(), height) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return time.Time{}, ErrBlockNotFound + return nil, ErrBlockNotFound } - return time.Time{}, err + return nil, err } - return block.Time, nil + return indexertypes.BlockFromDB(&block), nil +} + +// BlockByHeight returns the available information of the block with the given hash +func (idx *Indexer) BlockByHash(hash []byte) (*indexertypes.Block, error) { + block, err := idx.readOnlyQuery.GetBlockByHash(context.TODO(), hash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrBlockNotFound + } + return nil, err + } + return indexertypes.BlockFromDB(&block), nil } diff --git a/vochain/indexer/db/account.sql.go b/vochain/indexer/db/account.sql.go index c34ebc307..6e1691e5d 100644 --- a/vochain/indexer/db/account.sql.go +++ b/vochain/indexer/db/account.sql.go @@ -13,8 +13,6 @@ import ( ) const countAccounts = `-- name: CountAccounts :one -; - SELECT COUNT(*) FROM accounts ` @@ -42,9 +40,8 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (s } const getListAccounts = `-- name: GetListAccounts :many -; - -SELECT account, balance, nonce +SELECT account, balance, nonce, + COUNT(*) OVER() AS total_count FROM accounts ORDER BY balance DESC LIMIT ? OFFSET ? @@ -55,16 +52,28 @@ type GetListAccountsParams struct { Offset int64 } -func (q *Queries) GetListAccounts(ctx context.Context, arg GetListAccountsParams) ([]Account, error) { +type GetListAccountsRow struct { + Account types.AccountID + Balance int64 + Nonce int64 + TotalCount int64 +} + +func (q *Queries) GetListAccounts(ctx context.Context, arg GetListAccountsParams) ([]GetListAccountsRow, error) { rows, err := q.query(ctx, q.getListAccountsStmt, getListAccounts, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() - var items []Account + var items []GetListAccountsRow for rows.Next() { - var i Account - if err := rows.Scan(&i.Account, &i.Balance, &i.Nonce); err != nil { + var i GetListAccountsRow + if err := rows.Scan( + &i.Account, + &i.Balance, + &i.Nonce, + &i.TotalCount, + ); err != nil { return nil, err } items = append(items, i) diff --git a/vochain/indexer/db/blocks.sql.go b/vochain/indexer/db/blocks.sql.go index ffa3fa896..3c7f022fe 100644 --- a/vochain/indexer/db/blocks.sql.go +++ b/vochain/indexer/db/blocks.sql.go @@ -13,31 +13,68 @@ import ( const createBlock = `-- name: CreateBlock :execresult INSERT INTO blocks( - height, time, data_hash + chain_id, height, time, hash, proposer_address, last_block_hash ) VALUES ( - ?, ?, ? + ?, ?, ?, ?, ?, ? ) ` type CreateBlockParams struct { - Height int64 - Time time.Time - DataHash []byte + ChainID string + Height int64 + Time time.Time + Hash []byte + ProposerAddress []byte + LastBlockHash []byte } func (q *Queries) CreateBlock(ctx context.Context, arg CreateBlockParams) (sql.Result, error) { - return q.exec(ctx, q.createBlockStmt, createBlock, arg.Height, arg.Time, arg.DataHash) + return q.exec(ctx, q.createBlockStmt, createBlock, + arg.ChainID, + arg.Height, + arg.Time, + arg.Hash, + arg.ProposerAddress, + arg.LastBlockHash, + ) } -const getBlock = `-- name: GetBlock :one -SELECT height, time, data_hash FROM blocks +const getBlockByHash = `-- name: GetBlockByHash :one +SELECT height, time, chain_id, hash, proposer_address, last_block_hash FROM blocks +WHERE hash = ? +LIMIT 1 +` + +func (q *Queries) GetBlockByHash(ctx context.Context, hash []byte) (Block, error) { + row := q.queryRow(ctx, q.getBlockByHashStmt, getBlockByHash, hash) + var i Block + err := row.Scan( + &i.Height, + &i.Time, + &i.ChainID, + &i.Hash, + &i.ProposerAddress, + &i.LastBlockHash, + ) + return i, err +} + +const getBlockByHeight = `-- name: GetBlockByHeight :one +SELECT height, time, chain_id, hash, proposer_address, last_block_hash FROM blocks WHERE height = ? LIMIT 1 ` -func (q *Queries) GetBlock(ctx context.Context, height int64) (Block, error) { - row := q.queryRow(ctx, q.getBlockStmt, getBlock, height) +func (q *Queries) GetBlockByHeight(ctx context.Context, height int64) (Block, error) { + row := q.queryRow(ctx, q.getBlockByHeightStmt, getBlockByHeight, height) var i Block - err := row.Scan(&i.Height, &i.Time, &i.DataHash) + err := row.Scan( + &i.Height, + &i.Time, + &i.ChainID, + &i.Hash, + &i.ProposerAddress, + &i.LastBlockHash, + ) return i, err } diff --git a/vochain/indexer/db/db.go b/vochain/indexer/db/db.go index 9db9e8cdf..13ea9666d 100644 --- a/vochain/indexer/db/db.go +++ b/vochain/indexer/db/db.go @@ -36,6 +36,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.countTransactionsStmt, err = db.PrepareContext(ctx, countTransactions); err != nil { return nil, fmt.Errorf("error preparing query CountTransactions: %w", err) } + if q.countTransactionsByHeightStmt, err = db.PrepareContext(ctx, countTransactionsByHeight); err != nil { + return nil, fmt.Errorf("error preparing query CountTransactionsByHeight: %w", err) + } if q.countVotesStmt, err = db.PrepareContext(ctx, countVotes); err != nil { return nil, fmt.Errorf("error preparing query CountVotes: %w", err) } @@ -60,8 +63,11 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.createVoteStmt, err = db.PrepareContext(ctx, createVote); err != nil { return nil, fmt.Errorf("error preparing query CreateVote: %w", err) } - if q.getBlockStmt, err = db.PrepareContext(ctx, getBlock); err != nil { - return nil, fmt.Errorf("error preparing query GetBlock: %w", err) + if q.getBlockByHashStmt, err = db.PrepareContext(ctx, getBlockByHash); err != nil { + return nil, fmt.Errorf("error preparing query GetBlockByHash: %w", err) + } + if q.getBlockByHeightStmt, err = db.PrepareContext(ctx, getBlockByHeight); err != nil { + return nil, fmt.Errorf("error preparing query GetBlockByHeight: %w", err) } if q.getEntityCountStmt, err = db.PrepareContext(ctx, getEntityCount); err != nil { return nil, fmt.Errorf("error preparing query GetEntityCount: %w", err) @@ -132,6 +138,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.setProcessResultsReadyStmt, err = db.PrepareContext(ctx, setProcessResultsReady); err != nil { return nil, fmt.Errorf("error preparing query SetProcessResultsReady: %w", err) } + if q.transactionListByHeightStmt, err = db.PrepareContext(ctx, transactionListByHeight); err != nil { + return nil, fmt.Errorf("error preparing query TransactionListByHeight: %w", err) + } if q.updateProcessEndDateStmt, err = db.PrepareContext(ctx, updateProcessEndDate); err != nil { return nil, fmt.Errorf("error preparing query UpdateProcessEndDate: %w", err) } @@ -169,6 +178,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing countTransactionsStmt: %w", cerr) } } + if q.countTransactionsByHeightStmt != nil { + if cerr := q.countTransactionsByHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing countTransactionsByHeightStmt: %w", cerr) + } + } if q.countVotesStmt != nil { if cerr := q.countVotesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing countVotesStmt: %w", cerr) @@ -209,9 +223,14 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing createVoteStmt: %w", cerr) } } - if q.getBlockStmt != nil { - if cerr := q.getBlockStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing getBlockStmt: %w", cerr) + if q.getBlockByHashStmt != nil { + if cerr := q.getBlockByHashStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getBlockByHashStmt: %w", cerr) + } + } + if q.getBlockByHeightStmt != nil { + if cerr := q.getBlockByHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getBlockByHeightStmt: %w", cerr) } } if q.getEntityCountStmt != nil { @@ -329,6 +348,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing setProcessResultsReadyStmt: %w", cerr) } } + if q.transactionListByHeightStmt != nil { + if cerr := q.transactionListByHeightStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing transactionListByHeightStmt: %w", cerr) + } + } if q.updateProcessEndDateStmt != nil { if cerr := q.updateProcessEndDateStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateProcessEndDateStmt: %w", cerr) @@ -392,6 +416,7 @@ type Queries struct { countAccountsStmt *sql.Stmt countTokenTransfersByAccountStmt *sql.Stmt countTransactionsStmt *sql.Stmt + countTransactionsByHeightStmt *sql.Stmt countVotesStmt *sql.Stmt createAccountStmt *sql.Stmt createBlockStmt *sql.Stmt @@ -400,7 +425,8 @@ type Queries struct { createTokenTransferStmt *sql.Stmt createTransactionStmt *sql.Stmt createVoteStmt *sql.Stmt - getBlockStmt *sql.Stmt + getBlockByHashStmt *sql.Stmt + getBlockByHeightStmt *sql.Stmt getEntityCountStmt *sql.Stmt getLastTransactionsStmt *sql.Stmt getListAccountsStmt *sql.Stmt @@ -424,6 +450,7 @@ type Queries struct { searchVotesStmt *sql.Stmt setProcessResultsCancelledStmt *sql.Stmt setProcessResultsReadyStmt *sql.Stmt + transactionListByHeightStmt *sql.Stmt updateProcessEndDateStmt *sql.Stmt updateProcessFromStateStmt *sql.Stmt updateProcessResultByIDStmt *sql.Stmt @@ -432,47 +459,50 @@ type Queries struct { func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ - db: tx, - tx: tx, - computeProcessVoteCountStmt: q.computeProcessVoteCountStmt, - countAccountsStmt: q.countAccountsStmt, - countTokenTransfersByAccountStmt: q.countTokenTransfersByAccountStmt, - countTransactionsStmt: q.countTransactionsStmt, - countVotesStmt: q.countVotesStmt, - createAccountStmt: q.createAccountStmt, - createBlockStmt: q.createBlockStmt, - createProcessStmt: q.createProcessStmt, - createTokenFeeStmt: q.createTokenFeeStmt, - createTokenTransferStmt: q.createTokenTransferStmt, - createTransactionStmt: q.createTransactionStmt, - createVoteStmt: q.createVoteStmt, - getBlockStmt: q.getBlockStmt, - getEntityCountStmt: q.getEntityCountStmt, - getLastTransactionsStmt: q.getLastTransactionsStmt, - getListAccountsStmt: q.getListAccountsStmt, - getProcessStmt: q.getProcessStmt, - getProcessCountStmt: q.getProcessCountStmt, - getProcessIDsByFinalResultsStmt: q.getProcessIDsByFinalResultsStmt, - getProcessStatusStmt: q.getProcessStatusStmt, - getTokenFeesStmt: q.getTokenFeesStmt, - getTokenFeesByFromAccountStmt: q.getTokenFeesByFromAccountStmt, - getTokenFeesByReferenceStmt: q.getTokenFeesByReferenceStmt, - getTokenFeesByTxTypeStmt: q.getTokenFeesByTxTypeStmt, - getTokenTransferStmt: q.getTokenTransferStmt, - getTokenTransfersByFromAccountStmt: q.getTokenTransfersByFromAccountStmt, - getTokenTransfersByToAccountStmt: q.getTokenTransfersByToAccountStmt, - getTransactionStmt: q.getTransactionStmt, - getTransactionByHashStmt: q.getTransactionByHashStmt, + db: tx, + tx: tx, + computeProcessVoteCountStmt: q.computeProcessVoteCountStmt, + countAccountsStmt: q.countAccountsStmt, + countTokenTransfersByAccountStmt: q.countTokenTransfersByAccountStmt, + countTransactionsStmt: q.countTransactionsStmt, + countTransactionsByHeightStmt: q.countTransactionsByHeightStmt, + countVotesStmt: q.countVotesStmt, + createAccountStmt: q.createAccountStmt, + createBlockStmt: q.createBlockStmt, + createProcessStmt: q.createProcessStmt, + createTokenFeeStmt: q.createTokenFeeStmt, + createTokenTransferStmt: q.createTokenTransferStmt, + createTransactionStmt: q.createTransactionStmt, + createVoteStmt: q.createVoteStmt, + getBlockByHashStmt: q.getBlockByHashStmt, + getBlockByHeightStmt: q.getBlockByHeightStmt, + getEntityCountStmt: q.getEntityCountStmt, + getLastTransactionsStmt: q.getLastTransactionsStmt, + getListAccountsStmt: q.getListAccountsStmt, + getProcessStmt: q.getProcessStmt, + getProcessCountStmt: q.getProcessCountStmt, + getProcessIDsByFinalResultsStmt: q.getProcessIDsByFinalResultsStmt, + getProcessStatusStmt: q.getProcessStatusStmt, + getTokenFeesStmt: q.getTokenFeesStmt, + getTokenFeesByFromAccountStmt: q.getTokenFeesByFromAccountStmt, + getTokenFeesByReferenceStmt: q.getTokenFeesByReferenceStmt, + getTokenFeesByTxTypeStmt: q.getTokenFeesByTxTypeStmt, + getTokenTransferStmt: q.getTokenTransferStmt, + getTokenTransfersByFromAccountStmt: q.getTokenTransfersByFromAccountStmt, + getTokenTransfersByToAccountStmt: q.getTokenTransfersByToAccountStmt, + getTransactionStmt: q.getTransactionStmt, + getTransactionByHashStmt: q.getTransactionByHashStmt, getTxReferenceByBlockHeightAndBlockIndexStmt: q.getTxReferenceByBlockHeightAndBlockIndexStmt, - getVoteStmt: q.getVoteStmt, - searchEntitiesStmt: q.searchEntitiesStmt, - searchProcessesStmt: q.searchProcessesStmt, - searchVotesStmt: q.searchVotesStmt, - setProcessResultsCancelledStmt: q.setProcessResultsCancelledStmt, - setProcessResultsReadyStmt: q.setProcessResultsReadyStmt, - updateProcessEndDateStmt: q.updateProcessEndDateStmt, - updateProcessFromStateStmt: q.updateProcessFromStateStmt, - updateProcessResultByIDStmt: q.updateProcessResultByIDStmt, - updateProcessResultsStmt: q.updateProcessResultsStmt, + getVoteStmt: q.getVoteStmt, + searchEntitiesStmt: q.searchEntitiesStmt, + searchProcessesStmt: q.searchProcessesStmt, + searchVotesStmt: q.searchVotesStmt, + setProcessResultsCancelledStmt: q.setProcessResultsCancelledStmt, + setProcessResultsReadyStmt: q.setProcessResultsReadyStmt, + transactionListByHeightStmt: q.transactionListByHeightStmt, + updateProcessEndDateStmt: q.updateProcessEndDateStmt, + updateProcessFromStateStmt: q.updateProcessFromStateStmt, + updateProcessResultByIDStmt: q.updateProcessResultByIDStmt, + updateProcessResultsStmt: q.updateProcessResultsStmt, } } diff --git a/vochain/indexer/db/models.go b/vochain/indexer/db/models.go index a1566a32a..7b0ca88f1 100644 --- a/vochain/indexer/db/models.go +++ b/vochain/indexer/db/models.go @@ -10,16 +10,13 @@ import ( "go.vocdoni.io/dvote/types" ) -type Account struct { - Account types.AccountID - Balance int64 - Nonce int64 -} - type Block struct { - Height int64 - Time time.Time - DataHash []byte + Height int64 + Time time.Time + ChainID string + Hash []byte + ProposerAddress []byte + LastBlockHash []byte } type Process struct { @@ -78,4 +75,5 @@ type Transaction struct { BlockHeight int64 BlockIndex int64 Type string + RawTx []byte } diff --git a/vochain/indexer/db/processes.sql.go b/vochain/indexer/db/processes.sql.go index 73220d6b3..d15ffe39b 100644 --- a/vochain/indexer/db/processes.sql.go +++ b/vochain/indexer/db/processes.sql.go @@ -176,8 +176,6 @@ func (q *Queries) GetProcessCount(ctx context.Context) (int64, error) { } const getProcessIDsByFinalResults = `-- name: GetProcessIDsByFinalResults :many -; - SELECT id FROM processes WHERE final_results = ? ` @@ -219,27 +217,34 @@ func (q *Queries) GetProcessStatus(ctx context.Context, id types.ProcessID) (int } const searchEntities = `-- name: SearchEntities :many -SELECT entity_id, COUNT(id) AS process_count FROM processes -WHERE (?1 = '' OR (INSTR(LOWER(HEX(entity_id)), ?1) > 0)) +WITH results AS ( + SELECT id, entity_id, start_date, end_date, vote_count, chain_id, have_results, final_results, results_votes, results_weight, results_block_height, census_root, max_census_size, census_uri, metadata, census_origin, status, namespace, envelope, mode, vote_opts, private_keys, public_keys, question_index, creation_time, source_block_height, source_network_id, manually_ended, + COUNT(*) OVER() AS total_count + FROM processes + WHERE (?3 = '' OR (INSTR(LOWER(HEX(entity_id)), ?3) > 0)) +) +SELECT entity_id, COUNT(id) AS process_count, total_count +FROM results GROUP BY entity_id ORDER BY creation_time DESC, id ASC -LIMIT ?3 -OFFSET ?2 +LIMIT ?2 +OFFSET ?1 ` type SearchEntitiesParams struct { - EntityIDSubstr interface{} Offset int64 Limit int64 + EntityIDSubstr interface{} } type SearchEntitiesRow struct { - EntityID types.EntityID + EntityID []byte ProcessCount int64 + TotalCount int64 } func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams) ([]SearchEntitiesRow, error) { - rows, err := q.query(ctx, q.searchEntitiesStmt, searchEntities, arg.EntityIDSubstr, arg.Offset, arg.Limit) + rows, err := q.query(ctx, q.searchEntitiesStmt, searchEntities, arg.Offset, arg.Limit, arg.EntityIDSubstr) if err != nil { return nil, err } @@ -247,7 +252,7 @@ func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams) var items []SearchEntitiesRow for rows.Next() { var i SearchEntitiesRow - if err := rows.Scan(&i.EntityID, &i.ProcessCount); err != nil { + if err := rows.Scan(&i.EntityID, &i.ProcessCount, &i.TotalCount); err != nil { return nil, err } items = append(items, i) @@ -262,52 +267,63 @@ func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams) } const searchProcesses = `-- name: SearchProcesses :many -SELECT id FROM processes -WHERE (LENGTH(?1) = 0 OR entity_id = ?1) - AND (?2 = 0 OR namespace = ?2) - AND (?3 = 0 OR status = ?3) - AND (?4 = 0 OR source_network_id = ?4) - -- TODO(mvdan): consider keeping an id_hex column for faster searches - AND (?5 = '' OR (INSTR(LOWER(HEX(id)), ?5) > 0)) - AND (?6 = FALSE OR have_results) +WITH results AS ( + SELECT id, entity_id, start_date, end_date, vote_count, chain_id, have_results, final_results, results_votes, results_weight, results_block_height, census_root, max_census_size, census_uri, metadata, census_origin, status, namespace, envelope, mode, vote_opts, private_keys, public_keys, question_index, creation_time, source_block_height, source_network_id, manually_ended, + COUNT(*) OVER() AS total_count + FROM processes + WHERE (LENGTH(?3) = 0 OR entity_id = ?3) + AND (?4 = 0 OR namespace = ?4) + AND (?5 = 0 OR status = ?5) + AND (?6 = 0 OR source_network_id = ?6) + -- TODO: consider keeping an id_hex column for faster searches + AND (?7 = '' OR (INSTR(LOWER(HEX(id)), ?7) > 0)) + AND (?8 = FALSE OR have_results) +) +SELECT id, total_count +FROM results ORDER BY creation_time DESC, id ASC -LIMIT ?8 -OFFSET ?7 +LIMIT ?2 +OFFSET ?1 ` type SearchProcessesParams struct { + Offset int64 + Limit int64 EntityID interface{} Namespace interface{} Status interface{} SourceNetworkID interface{} IDSubstr interface{} WithResults interface{} - Offset int64 - Limit int64 } -func (q *Queries) SearchProcesses(ctx context.Context, arg SearchProcessesParams) ([]types.ProcessID, error) { +type SearchProcessesRow struct { + ID []byte + TotalCount int64 +} + +func (q *Queries) SearchProcesses(ctx context.Context, arg SearchProcessesParams) ([]SearchProcessesRow, error) { rows, err := q.query(ctx, q.searchProcessesStmt, searchProcesses, + arg.Offset, + arg.Limit, arg.EntityID, arg.Namespace, arg.Status, arg.SourceNetworkID, arg.IDSubstr, arg.WithResults, - arg.Offset, - arg.Limit, ) if err != nil { return nil, err } defer rows.Close() - var items []types.ProcessID + var items []SearchProcessesRow for rows.Next() { - var id types.ProcessID - if err := rows.Scan(&id); err != nil { + var i SearchProcessesRow + if err := rows.Scan(&i.ID, &i.TotalCount); err != nil { return nil, err } - items = append(items, id) + items = append(items, i) } if err := rows.Close(); err != nil { return nil, err @@ -382,8 +398,6 @@ func (q *Queries) UpdateProcessEndDate(ctx context.Context, arg UpdateProcessEnd } const updateProcessFromState = `-- name: UpdateProcessFromState :execresult -; - UPDATE processes SET census_root = ?1, census_uri = ?2, diff --git a/vochain/indexer/db/transactions.sql.go b/vochain/indexer/db/transactions.sql.go index c3d6e0b45..9861ca079 100644 --- a/vochain/indexer/db/transactions.sql.go +++ b/vochain/indexer/db/transactions.sql.go @@ -25,11 +25,23 @@ func (q *Queries) CountTransactions(ctx context.Context) (int64, error) { return count, err } +const countTransactionsByHeight = `-- name: CountTransactionsByHeight :one +SELECT COUNT(*) FROM transactions +WHERE block_height = ? +` + +func (q *Queries) CountTransactionsByHeight(ctx context.Context, blockHeight int64) (int64, error) { + row := q.queryRow(ctx, q.countTransactionsByHeightStmt, countTransactionsByHeight, blockHeight) + var count int64 + err := row.Scan(&count) + return count, err +} + const createTransaction = `-- name: CreateTransaction :execresult INSERT INTO transactions ( - hash, block_height, block_index, type + hash, block_height, block_index, type, raw_tx ) VALUES ( - ?, ?, ?, ? + ?, ?, ?, ?, ? ) ` @@ -38,6 +50,7 @@ type CreateTransactionParams struct { BlockHeight int64 BlockIndex int64 Type string + RawTx []byte } func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (sql.Result, error) { @@ -46,11 +59,12 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa arg.BlockHeight, arg.BlockIndex, arg.Type, + arg.RawTx, ) } const getLastTransactions = `-- name: GetLastTransactions :many -SELECT id, hash, block_height, block_index, type FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx FROM transactions ORDER BY id DESC LIMIT ? OFFSET ? @@ -76,6 +90,7 @@ func (q *Queries) GetLastTransactions(ctx context.Context, arg GetLastTransactio &i.BlockHeight, &i.BlockIndex, &i.Type, + &i.RawTx, ); err != nil { return nil, err } @@ -91,7 +106,7 @@ func (q *Queries) GetLastTransactions(ctx context.Context, arg GetLastTransactio } const getTransaction = `-- name: GetTransaction :one -SELECT id, hash, block_height, block_index, type FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx FROM transactions WHERE id = ? LIMIT 1 ` @@ -105,12 +120,13 @@ func (q *Queries) GetTransaction(ctx context.Context, id int64) (Transaction, er &i.BlockHeight, &i.BlockIndex, &i.Type, + &i.RawTx, ) return i, err } const getTransactionByHash = `-- name: GetTransactionByHash :one -SELECT id, hash, block_height, block_index, type FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx FROM transactions WHERE hash = ? LIMIT 1 ` @@ -124,12 +140,13 @@ func (q *Queries) GetTransactionByHash(ctx context.Context, hash types.Hash) (Tr &i.BlockHeight, &i.BlockIndex, &i.Type, + &i.RawTx, ) return i, err } const getTxReferenceByBlockHeightAndBlockIndex = `-- name: GetTxReferenceByBlockHeightAndBlockIndex :one -SELECT id, hash, block_height, block_index, type FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx FROM transactions WHERE block_height = ? AND block_index = ? LIMIT 1 ` @@ -148,6 +165,67 @@ func (q *Queries) GetTxReferenceByBlockHeightAndBlockIndex(ctx context.Context, &i.BlockHeight, &i.BlockIndex, &i.Type, + &i.RawTx, ) return i, err } + +const transactionListByHeight = `-- name: TransactionListByHeight :many +WITH results AS ( + SELECT id, hash, block_height, block_index, type, raw_tx, COUNT(*) OVER() AS total_count + FROM transactions + WHERE block_height = ? +) +SELECT id, hash, block_height, block_index, type, raw_tx, total_count +FROM results +ORDER BY block_index ASC +LIMIT ?3 +OFFSET ?2 +` + +type TransactionListByHeightParams struct { + BlockHeight int64 + Limit int64 + Offset int64 +} + +type TransactionListByHeightRow struct { + ID int64 + Hash []byte + BlockHeight int64 + BlockIndex int64 + Type string + RawTx []byte + TotalCount int64 +} + +func (q *Queries) TransactionListByHeight(ctx context.Context, arg TransactionListByHeightParams) ([]TransactionListByHeightRow, error) { + rows, err := q.query(ctx, q.transactionListByHeightStmt, transactionListByHeight, arg.BlockHeight, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TransactionListByHeightRow + for rows.Next() { + var i TransactionListByHeightRow + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.BlockHeight, + &i.BlockIndex, + &i.Type, + &i.RawTx, + &i.TotalCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/vochain/indexer/db/votes.sql.go b/vochain/indexer/db/votes.sql.go index b4925e0c1..29d901122 100644 --- a/vochain/indexer/db/votes.sql.go +++ b/vochain/indexer/db/votes.sql.go @@ -107,43 +107,49 @@ func (q *Queries) GetVote(ctx context.Context, nullifier types.Nullifier) (GetVo } const searchVotes = `-- name: SearchVotes :many -SELECT v.nullifier, v.process_id, v.block_height, v.block_index, v.weight, v.voter_id, v.overwrite_count, v.encryption_key_indexes, v.package, t.hash FROM votes AS v -LEFT JOIN transactions AS t - ON v.block_height = t.block_height - AND v.block_index = t.block_index -WHERE (?1 = '' OR process_id = ?1) - AND (?2 = '' OR (INSTR(LOWER(HEX(nullifier)), ?2) > 0)) -ORDER BY v.block_height DESC, v.nullifier ASC -LIMIT ?4 -OFFSET ?3 +WITH results AS ( + SELECT v.nullifier, v.process_id, v.block_height, v.block_index, v.weight, v.voter_id, v.overwrite_count, v.encryption_key_indexes, v.package, t.hash + FROM votes AS v + LEFT JOIN transactions AS t + ON v.block_height = t.block_height + AND v.block_index = t.block_index + WHERE (?3 = '' OR process_id = ?3) + AND (?4 = '' OR (INSTR(LOWER(HEX(nullifier)), ?4) > 0)) +) +SELECT nullifier, process_id, block_height, block_index, weight, voter_id, overwrite_count, encryption_key_indexes, package, hash, COUNT(*) OVER() AS total_count +FROM results +ORDER BY block_height DESC, nullifier ASC +LIMIT ?2 +OFFSET ?1 ` type SearchVotesParams struct { - ProcessID interface{} - NullifierSubstr interface{} Offset int64 Limit int64 + ProcessID interface{} + NullifierSubstr interface{} } type SearchVotesRow struct { - Nullifier types.Nullifier - ProcessID types.ProcessID + Nullifier []byte + ProcessID []byte BlockHeight int64 BlockIndex int64 Weight string - VoterID state.VoterID + VoterID []byte OverwriteCount int64 EncryptionKeyIndexes string Package string - Hash types.Hash + Hash []byte + TotalCount int64 } func (q *Queries) SearchVotes(ctx context.Context, arg SearchVotesParams) ([]SearchVotesRow, error) { rows, err := q.query(ctx, q.searchVotesStmt, searchVotes, - arg.ProcessID, - arg.NullifierSubstr, arg.Offset, arg.Limit, + arg.ProcessID, + arg.NullifierSubstr, ) if err != nil { return nil, err @@ -163,6 +169,7 @@ func (q *Queries) SearchVotes(ctx context.Context, arg SearchVotesParams) ([]Sea &i.EncryptionKeyIndexes, &i.Package, &i.Hash, + &i.TotalCount, ); err != nil { return nil, err } diff --git a/vochain/indexer/indexer.go b/vochain/indexer/indexer.go index c0b6d0289..6efac5a15 100644 --- a/vochain/indexer/indexer.go +++ b/vochain/indexer/indexer.go @@ -414,6 +414,20 @@ func (idx *Indexer) Commit(height uint32) error { queries := idx.blockTxQueries() ctx := context.TODO() + // index the new block + if b := idx.App.GetBlockByHeight(int64(height)); b != nil { + if _, err := queries.CreateBlock(context.TODO(), indexerdb.CreateBlockParams{ + ChainID: b.ChainID, + Height: b.Height, + Time: b.Time, + Hash: nonNullBytes(b.Hash()), + ProposerAddress: nonNullBytes(b.ProposerAddress), + LastBlockHash: nonNullBytes(b.LastBlockID.Hash), + }); err != nil { + log.Errorw(err, "cannot index new block") + } + } + for _, pidStr := range updateProcs { pid := types.ProcessID(pidStr) if err := idx.updateProcess(ctx, queries, pid); err != nil { @@ -905,21 +919,24 @@ func (idx *Indexer) CountTotalAccounts() (uint64, error) { return uint64(count), err } -func (idx *Indexer) GetListAccounts(offset, maxItems int32) ([]indexertypes.Account, error) { - accsFromDB, err := idx.readOnlyQuery.GetListAccounts(context.TODO(), indexerdb.GetListAccountsParams{ +func (idx *Indexer) AccountsList(offset, maxItems int) ([]indexertypes.Account, uint64, error) { + results, err := idx.readOnlyQuery.GetListAccounts(context.TODO(), indexerdb.GetListAccountsParams{ Limit: int64(maxItems), Offset: int64(offset), }) if err != nil { - return nil, err + return nil, 0, err + } + if len(results) == 0 { + return []indexertypes.Account{}, 0, nil } - tt := []indexertypes.Account{} - for _, acc := range accsFromDB { - tt = append(tt, indexertypes.Account{ - Address: acc.Account, - Balance: uint64(acc.Balance), - Nonce: uint32(acc.Nonce), + list := []indexertypes.Account{} + for _, row := range results { + list = append(list, indexertypes.Account{ + Address: row.Account, + Balance: uint64(row.Balance), + Nonce: uint32(row.Nonce), }) } - return tt, nil + return list, uint64(results[0].TotalCount), nil } diff --git a/vochain/indexer/indexer_test.go b/vochain/indexer/indexer_test.go index e5be9e659..e67b369a4 100644 --- a/vochain/indexer/indexer_test.go +++ b/vochain/indexer/indexer_test.go @@ -161,7 +161,8 @@ func testEntityList(t *testing.T, entityCount int) { entitiesByID := make(map[string]bool) last := 0 for len(entitiesByID) <= entityCount { - list := idx.EntityList(10, last, "") + list, _, err := idx.EntityList(last, 10, "") + qt.Assert(t, err, qt.IsNil) if len(list) < 1 { t.Log("list is empty") break @@ -255,19 +256,24 @@ func TestEntitySearch(t *testing.T) { } app.AdvanceTestBlock() // Exact entity search - list := idx.EntityList(10, 0, "4011d50537fa164b6fef261141797bbe4014526e") + list, _, err := idx.EntityList(0, 10, "4011d50537fa164b6fef261141797bbe4014526e") + qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, 1) // Search for nonexistent entity - list = idx.EntityList(10, 0, "4011d50537fa164b6fef261141797bbe4014526f") + list, _, err = idx.EntityList(0, 10, "4011d50537fa164b6fef261141797bbe4014526f") + qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, 0) // Search containing part of all manually-defined entities - list = idx.EntityList(10, 0, "011d50537fa164b6fef261141797bbe4014526e") + list, _, err = idx.EntityList(0, 10, "011d50537fa164b6fef261141797bbe4014526e") + qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, len(entityIds)) // Partial entity search as mixed case hex - list = idx.EntityList(10, 0, "50537FA164B6Fef261141797BbE401452") + list, _, err = idx.EntityList(0, 10, "50537FA164B6Fef261141797BbE401452") + qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, len(entityIds)) // Partial entity search as uppercase hex - list = idx.EntityList(10, 0, "50537FA164B6FEF261141797BBE401452") + list, _, err = idx.EntityList(0, 10, "50537FA164B6FEF261141797BBE401452") + qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, len(entityIds)) } @@ -324,10 +330,11 @@ func testProcessList(t *testing.T, procsCount int) { procs := make(map[string]bool) last := 0 for len(procs) < procsCount { - list, err := idx.ProcessList(eidProcsCount, last, 10, "", 0, 0, "", false) + list, total, err := idx.ProcessList(eidProcsCount, last, 10, "", 0, 0, 0, false) if err != nil { t.Fatal(err) } + qt.Assert(t, total, qt.Equals, uint64(procsCount)) if len(list) < 1 { t.Log("list is empty") break @@ -342,12 +349,14 @@ func testProcessList(t *testing.T, procsCount int) { } qt.Assert(t, procs, qt.HasLen, procsCount) - _, err := idx.ProcessList(nil, 0, 64, "", 0, 0, "", false) + _, total, err := idx.ProcessList(nil, 0, 64, "", 0, 0, 0, false) qt.Assert(t, err, qt.IsNil) + qt.Assert(t, total, qt.Equals, uint64(10+procsCount)) qt.Assert(t, idx.CountTotalProcesses(), qt.Equals, uint64(10+procsCount)) countEntityProcs := func(eid []byte) int64 { - list := idx.EntityList(1, 0, fmt.Sprintf("%x", eid)) + list, _, err := idx.EntityList(0, 1, fmt.Sprintf("%x", eid)) + qt.Assert(t, err, qt.IsNil) if len(list) == 0 { return -1 } @@ -356,6 +365,11 @@ func testProcessList(t *testing.T, procsCount int) { qt.Assert(t, countEntityProcs(eidOneProcess), qt.Equals, int64(1)) qt.Assert(t, countEntityProcs(eidProcsCount), qt.Equals, int64(procsCount)) qt.Assert(t, countEntityProcs([]byte("not an entity id that exists")), qt.Equals, int64(-1)) + + // Past the end (from=10000) should return an empty list + emptyList, _, err := idx.ProcessList(nil, 10000, 64, "", 0, 0, 0, false) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, emptyList, qt.DeepEquals, [][]byte{}) } func TestProcessSearch(t *testing.T) { @@ -443,7 +457,7 @@ func TestProcessSearch(t *testing.T) { app.AdvanceTestBlock() // Exact process search - list, err := idx.ProcessList(eidTest, 0, 10, pidExact, 0, 0, "", false) + list, _, err := idx.ProcessList(eidTest, 0, 10, pidExact, 0, 0, 0, false) if err != nil { t.Fatal(err) } @@ -452,7 +466,7 @@ func TestProcessSearch(t *testing.T) { } // Exact process search, with it being encrypted. // This once caused a sqlite bug due to a mistake in the SQL query. - list, err = idx.ProcessList(eidTest, 0, 10, pidExactEncrypted, 0, 0, "", false) + list, _, err = idx.ProcessList(eidTest, 0, 10, pidExactEncrypted, 0, 0, 0, false) if err != nil { t.Fatal(err) } @@ -460,8 +474,8 @@ func TestProcessSearch(t *testing.T) { t.Fatalf("expected 1 process, got %d", len(list)) } // Search for nonexistent process - list, err = idx.ProcessList(eidTest, 0, 10, - "4011d50537fa164b6fef261141797bbe4014526f", 0, 0, "", false) + list, _, err = idx.ProcessList(eidTest, 0, 10, + "4011d50537fa164b6fef261141797bbe4014526f", 0, 0, 0, false) if err != nil { t.Fatal(err) } @@ -469,8 +483,8 @@ func TestProcessSearch(t *testing.T) { t.Fatalf("expected 0 processes, got %d", len(list)) } // Search containing part of all manually-defined processes - list, err = idx.ProcessList(eidTest, 0, 10, - "011d50537fa164b6fef261141797bbe4014526e", 0, 0, "", false) + list, _, err = idx.ProcessList(eidTest, 0, 10, + "011d50537fa164b6fef261141797bbe4014526e", 0, 0, 0, false) if err != nil { t.Fatal(err) } @@ -478,8 +492,8 @@ func TestProcessSearch(t *testing.T) { t.Fatalf("expected %d processes, got %d", len(processIds), len(list)) } - list, err = idx.ProcessList(eidTest, 0, 100, - "0c6ca22d2c175a1fbdd15d7595ae532bb1094b5", 0, 0, "ENDED", false) + list, _, err = idx.ProcessList(eidTest, 0, 100, + "0c6ca22d2c175a1fbdd15d7595ae532bb1094b5", 0, 0, models.ProcessStatus_ENDED, false) if err != nil { t.Fatal(err) } @@ -488,17 +502,17 @@ func TestProcessSearch(t *testing.T) { } // Partial process search as uppercase hex - list, err = idx.ProcessList(eidTest, 0, 10, "011D50537FA164B6FEF261141797BBE4014526E", 0, 0, "", false) + list, _, err = idx.ProcessList(eidTest, 0, 10, "011D50537FA164B6FEF261141797BBE4014526E", 0, 0, 0, false) qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, len(processIds)) // Partial process search as mixed case hex - list, err = idx.ProcessList(eidTest, 0, 10, "011D50537fA164B6FeF261141797BbE4014526E", 0, 0, "", false) + list, _, err = idx.ProcessList(eidTest, 0, 10, "011D50537fA164B6FeF261141797BbE4014526E", 0, 0, 0, false) qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, len(processIds)) // Search with an exact Entity ID, but starting with a null byte. // This can trip up sqlite, as it assumes TEXT strings are NUL-terminated. - list, err = idx.ProcessList([]byte("\x00foobar"), 0, 100, "", 0, 0, "", false) + list, _, err = idx.ProcessList([]byte("\x00foobar"), 0, 100, "", 0, 0, 0, false) if err != nil { t.Fatal(err) } @@ -507,12 +521,12 @@ func TestProcessSearch(t *testing.T) { } // list all processes, with a max of 10 - list, err = idx.ProcessList(nil, 0, 10, "", 0, 0, "", false) + list, _, err = idx.ProcessList(nil, 0, 10, "", 0, 0, 0, false) qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, 10) // list all processes, with a max of 1000 - list, err = idx.ProcessList(nil, 0, 1000, "", 0, 0, "", false) + list, _, err = idx.ProcessList(nil, 0, 1000, "", 0, 0, 0, false) qt.Assert(t, err, qt.IsNil) qt.Assert(t, list, qt.HasLen, 21) } @@ -561,25 +575,25 @@ func TestProcessListWithNamespaceAndStatus(t *testing.T) { app.AdvanceTestBlock() // Get the process list for namespace 123 - list, err := idx.ProcessList(eid20, 0, 100, "", 123, 0, "", false) + list, _, err := idx.ProcessList(eid20, 0, 100, "", 123, 0, 0, false) qt.Assert(t, err, qt.IsNil) // Check there are exactly 10 qt.Assert(t, len(list), qt.CmpEquals(), 10) // Get the process list for all namespaces - list, err = idx.ProcessList(nil, 0, 100, "", 0, 0, "", false) + list, _, err = idx.ProcessList(nil, 0, 100, "", 0, 0, 0, false) qt.Assert(t, err, qt.IsNil) // Check there are exactly 10 + 10 qt.Assert(t, len(list), qt.CmpEquals(), 20) // Get the process list for namespace 10 - list, err = idx.ProcessList(nil, 0, 100, "", 10, 0, "", false) + list, _, err = idx.ProcessList(nil, 0, 100, "", 10, 0, 0, false) qt.Assert(t, err, qt.IsNil) // Check there is exactly 1 qt.Assert(t, len(list), qt.CmpEquals(), 1) // Get the process list for namespace 10 - list, err = idx.ProcessList(nil, 0, 100, "", 0, 0, "READY", false) + list, _, err = idx.ProcessList(nil, 0, 100, "", 0, 0, models.ProcessStatus_READY, false) qt.Assert(t, err, qt.IsNil) // Check there is exactly 1 qt.Assert(t, len(list), qt.CmpEquals(), 10) @@ -681,8 +695,8 @@ func TestResults(t *testing.T) { // Update the process app.AdvanceTestBlock() - // GetEnvelopes with a limit - envelopes, err := idx.GetEnvelopes(pid, 10, 0, "") + // VoteList with a limit + envelopes, _, err := idx.VoteList(pid, 10, 0, "") qt.Assert(t, err, qt.IsNil) qt.Assert(t, envelopes, qt.HasLen, 10) qt.Assert(t, envelopes[0].Height, qt.Equals, uint32(30)) @@ -692,32 +706,32 @@ func TestResults(t *testing.T) { matchNullifier := fmt.Sprintf("%x", envelopes[9].Nullifier) matchHeight := envelopes[9].Height - // GetEnvelopes with a limit and offset - envelopes, err = idx.GetEnvelopes(pid, 5, 27, "") + // VoteList with a limit and offset + envelopes, _, err = idx.VoteList(pid, 5, 27, "") qt.Assert(t, err, qt.IsNil) qt.Assert(t, envelopes, qt.HasLen, 3) qt.Assert(t, envelopes[0].Height, qt.Equals, uint32(3)) qt.Assert(t, envelopes[2].Height, qt.Equals, uint32(1)) - // GetEnvelopes without a match - envelopes, err = idx.GetEnvelopes(pid, 10, 0, fmt.Sprintf("%x", util.RandomBytes(32))) + // VoteList without a match + envelopes, _, err = idx.VoteList(pid, 10, 0, fmt.Sprintf("%x", util.RandomBytes(32))) qt.Assert(t, err, qt.IsNil) qt.Assert(t, envelopes, qt.HasLen, 0) - // GetEnvelopes with one match by full nullifier - envelopes, err = idx.GetEnvelopes(pid, 10, 0, matchNullifier) + // VoteList with one match by full nullifier + envelopes, _, err = idx.VoteList(pid, 10, 0, matchNullifier) qt.Assert(t, err, qt.IsNil) qt.Assert(t, envelopes, qt.HasLen, 1) qt.Assert(t, envelopes[0].Height, qt.Equals, matchHeight) - // GetEnvelopes with one match by partial nullifier - envelopes, err = idx.GetEnvelopes(pid, 10, 0, matchNullifier[:29]) + // VoteList with one match by partial nullifier + envelopes, _, err = idx.VoteList(pid, 10, 0, matchNullifier[:29]) qt.Assert(t, err, qt.IsNil) qt.Assert(t, envelopes, qt.HasLen, 1) qt.Assert(t, envelopes[0].Height, qt.Equals, matchHeight) // Partial vote search as uppercase hex - envelopes, err = idx.GetEnvelopes(pid, 10, 0, strings.ToUpper(matchNullifier[:29])) + envelopes, _, err = idx.VoteList(pid, 10, 0, strings.ToUpper(matchNullifier[:29])) qt.Assert(t, err, qt.IsNil) qt.Assert(t, envelopes, qt.HasLen, 1) qt.Assert(t, envelopes[0].Height, qt.Equals, matchHeight) @@ -1365,6 +1379,7 @@ func TestTxIndexer(t *testing.T) { idx.OnNewTx(&vochaintx.Tx{ TxID: getTxID(i, j), TxModelType: "setAccount", + Tx: &models.Tx{Payload: &models.Tx_SetAccount{}}, }, uint32(i), int32(j)) } } @@ -1392,7 +1407,7 @@ func TestTxIndexer(t *testing.T) { } } - txs, err := idx.GetLastTransactions(15, 0) + txs, _, err := idx.GetLastTransactions(15, 0) qt.Assert(t, err, qt.IsNil) for i, tx := range txs { // Index is between 1 and totalCount. @@ -1403,7 +1418,7 @@ func TestTxIndexer(t *testing.T) { qt.Assert(t, tx.TxType, qt.Equals, "setAccount") } - txs, err = idx.GetLastTransactions(1, 5) + txs, _, err = idx.GetLastTransactions(1, 5) qt.Assert(t, err, qt.IsNil) qt.Assert(t, txs, qt.HasLen, 1) qt.Assert(t, txs[0].Index, qt.Equals, uint64(95)) @@ -1537,7 +1552,7 @@ func TestAccountsList(t *testing.T) { last := 0 for i := 0; i < int(totalAccs); i++ { - accts, err := idx.GetListAccounts(int32(last), 10) + accts, _, err := idx.AccountsList(last, 10) qt.Assert(t, err, qt.IsNil) for j, acc := range accts { @@ -1560,7 +1575,7 @@ func TestAccountsList(t *testing.T) { app.AdvanceTestBlock() // verify the updated balance and nonce - accts, err := idx.GetListAccounts(int32(0), 5) + accts, _, err := idx.AccountsList(0, 5) qt.Assert(t, err, qt.IsNil) // the account in the position 0 must be the updated account balance due it has the major balance // indexer query has order BY balance DESC diff --git a/vochain/indexer/indexertypes/block.go b/vochain/indexer/indexertypes/block.go new file mode 100644 index 000000000..9864f4101 --- /dev/null +++ b/vochain/indexer/indexertypes/block.go @@ -0,0 +1,32 @@ +package indexertypes + +import ( + "time" + + "go.vocdoni.io/dvote/types" + indexerdb "go.vocdoni.io/dvote/vochain/indexer/db" +) + +// Block represents a block handled by the Vochain. +// The indexer Block data type is different from the vochain state data type +// since it is optimized for querying purposes and not for keeping a shared consensus state. +type Block struct { + ChainID string `json:"chainId"` + Height int64 `json:"height"` + Time time.Time `json:"time"` + Hash types.HexBytes `json:"hash"` + ProposerAddress types.HexBytes `json:"proposer"` + LastBlockHash types.HexBytes `json:"lastBlockHash"` +} + +// BlockFromDB converts the indexerdb.Block into a Block +func BlockFromDB(dbblock *indexerdb.Block) *Block { + return &Block{ + ChainID: dbblock.ChainID, + Height: dbblock.Height, + Time: dbblock.Time, + Hash: nonEmptyBytes(dbblock.Hash), + ProposerAddress: nonEmptyBytes(dbblock.ProposerAddress), + LastBlockHash: nonEmptyBytes(dbblock.LastBlockHash), + } +} diff --git a/vochain/indexer/indexertypes/types.go b/vochain/indexer/indexertypes/types.go index 31ba97d73..cc25e97ab 100644 --- a/vochain/indexer/indexertypes/types.go +++ b/vochain/indexer/indexertypes/types.go @@ -191,6 +191,7 @@ type Transaction struct { BlockHeight uint32 `json:"blockHeight" format:"int32" example:"64924"` TxBlockIndex int32 `json:"transactionIndex" format:"int32" example:"0"` TxType string `json:"transactionType" enums:"vote,newProcess,admin,setProcess,registerKey,mintTokens,sendTokens,setTransactionCosts,setAccount,collectFaucet,setKeykeeper" example:"Vote"` + RawTx types.HexBytes `json:"rawTx"` } func TransactionFromDB(dbtx *indexerdb.Transaction) *Transaction { @@ -200,6 +201,18 @@ func TransactionFromDB(dbtx *indexerdb.Transaction) *Transaction { BlockHeight: uint32(dbtx.BlockHeight), TxBlockIndex: int32(dbtx.BlockIndex), TxType: dbtx.Type, + RawTx: dbtx.RawTx, + } +} + +func TransactionFromDBRow(dbtx *indexerdb.TransactionListByHeightRow) *Transaction { + return &Transaction{ + Index: uint64(dbtx.ID), + Hash: dbtx.Hash, + BlockHeight: uint32(dbtx.BlockHeight), + TxBlockIndex: int32(dbtx.BlockIndex), + TxType: dbtx.Type, + RawTx: dbtx.RawTx, } } @@ -236,3 +249,8 @@ type TokenTransfersAccount struct { Received []*TokenTransferMeta `json:"received"` Sent []*TokenTransferMeta `json:"sent"` } + +type Entity struct { + EntityID types.EntityID + ProcessCount int64 +} diff --git a/vochain/indexer/migrations/0013_alter_columns_table_blocks.sql b/vochain/indexer/migrations/0013_alter_columns_table_blocks.sql new file mode 100644 index 000000000..c83c32c06 --- /dev/null +++ b/vochain/indexer/migrations/0013_alter_columns_table_blocks.sql @@ -0,0 +1,13 @@ +-- +goose Up +ALTER TABLE blocks DROP COLUMN data_hash; +ALTER TABLE blocks ADD COLUMN chain_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE blocks ADD COLUMN hash BLOB NOT NULL DEFAULT x''; +ALTER TABLE blocks ADD COLUMN proposer_address BLOB NOT NULL DEFAULT x''; +ALTER TABLE blocks ADD COLUMN last_block_hash BLOB NOT NULL DEFAULT x''; + +-- +goose Down +ALTER TABLE blocks ADD COLUMN data_hash BLOB NOT NULL; +ALTER TABLE blocks DROP COLUMN chain_id; +ALTER TABLE blocks DROP COLUMN hash; +ALTER TABLE blocks DROP COLUMN proposer_address; +ALTER TABLE blocks DROP COLUMN last_block_hash; diff --git a/vochain/indexer/migrations/0014_alter_columns_table_transactions.sql b/vochain/indexer/migrations/0014_alter_columns_table_transactions.sql new file mode 100644 index 000000000..b1302265c --- /dev/null +++ b/vochain/indexer/migrations/0014_alter_columns_table_transactions.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE transactions ADD COLUMN raw_tx BLOB NOT NULL DEFAULT x''; + +-- +goose Down +ALTER TABLE transactions DROP COLUMN raw_tx; diff --git a/vochain/indexer/process.go b/vochain/indexer/process.go index 3a103cc3b..098776f88 100644 --- a/vochain/indexer/process.go +++ b/vochain/indexer/process.go @@ -49,29 +49,19 @@ func (idx *Indexer) ProcessInfo(pid []byte) (*indexertypes.Process, error) { // declared as zero-values will be ignored. SearchTerm is a partial or full PID. // Status is one of READY, CANCELED, ENDED, PAUSED, RESULTS func (idx *Indexer) ProcessList(entityID []byte, from, max int, searchTerm string, namespace uint32, - srcNetworkId int32, status string, withResults bool, -) ([][]byte, error) { + srcNetworkId int32, status models.ProcessStatus, withResults bool, +) ([][]byte, uint64, error) { if from < 0 { - return nil, fmt.Errorf("processList: invalid value: from is invalid value %d", from) - } - // For filtering on Status we use a badgerhold match function. - // If status is not defined, then the match function will return always true. - statusnum := int32(0) - statusfound := false - if status != "" { - if statusnum, statusfound = models.ProcessStatus_value[status]; !statusfound { - return nil, fmt.Errorf("processList: status %s is unknown", status) - } + return nil, 0, fmt.Errorf("processList: invalid value: from is invalid value %d", from) } // Filter match function for source network Id if _, ok := models.SourceNetworkId_name[srcNetworkId]; !ok { - return nil, fmt.Errorf("sourceNetworkId is unknown %d", srcNetworkId) + return nil, 0, fmt.Errorf("sourceNetworkId is unknown %d", srcNetworkId) } - - procs, err := idx.readOnlyQuery.SearchProcesses(context.TODO(), indexerdb.SearchProcessesParams{ + results, err := idx.readOnlyQuery.SearchProcesses(context.TODO(), indexerdb.SearchProcessesParams{ EntityID: nonNullBytes(entityID), // so that LENGTH never returns NULL Namespace: int64(namespace), - Status: int64(statusnum), + Status: int64(status), SourceNetworkID: int64(srcNetworkId), IDSubstr: strings.ToLower(searchTerm), // we search in lowercase Offset: int64(from), @@ -79,9 +69,16 @@ func (idx *Indexer) ProcessList(entityID []byte, from, max int, searchTerm strin WithResults: withResults, }) if err != nil { - return nil, err + return nil, 0, err + } + if len(results) == 0 { + return [][]byte{}, 0, nil } - return procs, nil + list := [][]byte{} + for _, row := range results { + list = append(list, row.ID) + } + return list, uint64(results[0].TotalCount), nil } // CountTotalProcesses returns the total number of processes indexed. @@ -97,17 +94,26 @@ func (idx *Indexer) CountTotalProcesses() uint64 { // EntityList returns the list of entities indexed by the indexer // searchTerm is optional, if declared as zero-value // will be ignored. Searches against the ID field as lowercase hex. -func (idx *Indexer) EntityList(max, from int, searchTerm string) []indexerdb.SearchEntitiesRow { - rows, err := idx.readOnlyQuery.SearchEntities(context.TODO(), indexerdb.SearchEntitiesParams{ +func (idx *Indexer) EntityList(from, max int, searchTerm string) ([]indexertypes.Entity, uint64, error) { + results, err := idx.readOnlyQuery.SearchEntities(context.TODO(), indexerdb.SearchEntitiesParams{ EntityIDSubstr: strings.ToLower(searchTerm), // we search in lowercase Offset: int64(from), Limit: int64(max), }) if err != nil { - log.Errorf("error listing entities: %v", err) - return nil + return nil, 0, fmt.Errorf("error listing entities: %w", err) + } + if len(results) == 0 { + return nil, 0, nil + } + list := []indexertypes.Entity{} + for _, row := range results { + list = append(list, indexertypes.Entity{ + EntityID: row.EntityID, + ProcessCount: row.ProcessCount, + }) } - return rows + return list, uint64(results[0].TotalCount), nil } // CountTotalEntities return the total number of entities indexed by the indexer diff --git a/vochain/indexer/queries/account.sql b/vochain/indexer/queries/account.sql index e459ff375..630e14657 100644 --- a/vochain/indexer/queries/account.sql +++ b/vochain/indexer/queries/account.sql @@ -1,15 +1,14 @@ -- name: CreateAccount :execresult REPLACE INTO accounts ( account, balance, nonce -) VALUES (?, ?, ?) -; +) VALUES (?, ?, ?); -- name: GetListAccounts :many -SELECT * +SELECT *, + COUNT(*) OVER() AS total_count FROM accounts ORDER BY balance DESC -LIMIT ? OFFSET ? -; +LIMIT ? OFFSET ?; -- name: CountAccounts :one SELECT COUNT(*) FROM accounts; \ No newline at end of file diff --git a/vochain/indexer/queries/blocks.sql b/vochain/indexer/queries/blocks.sql index 577e875b5..31bf9da79 100644 --- a/vochain/indexer/queries/blocks.sql +++ b/vochain/indexer/queries/blocks.sql @@ -1,11 +1,16 @@ -- name: CreateBlock :execresult INSERT INTO blocks( - height, time, data_hash + chain_id, height, time, hash, proposer_address, last_block_hash ) VALUES ( - ?, ?, ? + ?, ?, ?, ?, ?, ? ); --- name: GetBlock :one +-- name: GetBlockByHeight :one SELECT * FROM blocks WHERE height = ? LIMIT 1; + +-- name: GetBlockByHash :one +SELECT * FROM blocks +WHERE hash = ? +LIMIT 1; diff --git a/vochain/indexer/queries/processes.sql b/vochain/indexer/queries/processes.sql index 919eeccab..bc57edce4 100644 --- a/vochain/indexer/queries/processes.sql +++ b/vochain/indexer/queries/processes.sql @@ -31,18 +31,23 @@ WHERE id = ? LIMIT 1; -- name: SearchProcesses :many -SELECT id FROM processes -WHERE (LENGTH(sqlc.arg(entity_id)) = 0 OR entity_id = sqlc.arg(entity_id)) - AND (sqlc.arg(namespace) = 0 OR namespace = sqlc.arg(namespace)) - AND (sqlc.arg(status) = 0 OR status = sqlc.arg(status)) - AND (sqlc.arg(source_network_id) = 0 OR source_network_id = sqlc.arg(source_network_id)) - -- TODO(mvdan): consider keeping an id_hex column for faster searches - AND (sqlc.arg(id_substr) = '' OR (INSTR(LOWER(HEX(id)), sqlc.arg(id_substr)) > 0)) - AND (sqlc.arg(with_results) = FALSE OR have_results) +WITH results AS ( + SELECT *, + COUNT(*) OVER() AS total_count + FROM processes + WHERE (LENGTH(sqlc.arg(entity_id)) = 0 OR entity_id = sqlc.arg(entity_id)) + AND (sqlc.arg(namespace) = 0 OR namespace = sqlc.arg(namespace)) + AND (sqlc.arg(status) = 0 OR status = sqlc.arg(status)) + AND (sqlc.arg(source_network_id) = 0 OR source_network_id = sqlc.arg(source_network_id)) + -- TODO: consider keeping an id_hex column for faster searches + AND (sqlc.arg(id_substr) = '' OR (INSTR(LOWER(HEX(id)), sqlc.arg(id_substr)) > 0)) + AND (sqlc.arg(with_results) = FALSE OR have_results) +) +SELECT id, total_count +FROM results ORDER BY creation_time DESC, id ASC LIMIT sqlc.arg(limit) -OFFSET sqlc.arg(offset) -; +OFFSET sqlc.arg(offset); -- name: UpdateProcessFromState :execresult UPDATE processes @@ -96,13 +101,18 @@ SELECT COUNT(*) FROM processes; SELECT COUNT(DISTINCT entity_id) FROM processes; -- name: SearchEntities :many -SELECT entity_id, COUNT(id) AS process_count FROM processes -WHERE (sqlc.arg(entity_id_substr) = '' OR (INSTR(LOWER(HEX(entity_id)), sqlc.arg(entity_id_substr)) > 0)) +WITH results AS ( + SELECT *, + COUNT(*) OVER() AS total_count + FROM processes + WHERE (sqlc.arg(entity_id_substr) = '' OR (INSTR(LOWER(HEX(entity_id)), sqlc.arg(entity_id_substr)) > 0)) +) +SELECT entity_id, COUNT(id) AS process_count, total_count +FROM results GROUP BY entity_id ORDER BY creation_time DESC, id ASC LIMIT sqlc.arg(limit) -OFFSET sqlc.arg(offset) -; +OFFSET sqlc.arg(offset); -- name: GetProcessIDsByFinalResults :many SELECT id FROM processes diff --git a/vochain/indexer/queries/transactions.sql b/vochain/indexer/queries/transactions.sql index 5b5ec4698..d56683233 100644 --- a/vochain/indexer/queries/transactions.sql +++ b/vochain/indexer/queries/transactions.sql @@ -1,8 +1,8 @@ -- name: CreateTransaction :execresult INSERT INTO transactions ( - hash, block_height, block_index, type + hash, block_height, block_index, type, raw_tx ) VALUES ( - ?, ?, ?, ? + ?, ?, ?, ?, ? ); -- name: GetTransaction :one @@ -25,8 +25,23 @@ OFFSET ? -- name: CountTransactions :one SELECT COUNT(*) FROM transactions; +-- name: CountTransactionsByHeight :one +SELECT COUNT(*) FROM transactions +WHERE block_height = ?; + -- name: GetTxReferenceByBlockHeightAndBlockIndex :one SELECT * FROM transactions WHERE block_height = ? AND block_index = ? LIMIT 1; +-- name: TransactionListByHeight :many +WITH results AS ( + SELECT *, COUNT(*) OVER() AS total_count + FROM transactions + WHERE block_height = ? +) +SELECT * +FROM results +ORDER BY block_index ASC +LIMIT sqlc.arg(limit) +OFFSET sqlc.arg(offset); \ No newline at end of file diff --git a/vochain/indexer/queries/votes.sql b/vochain/indexer/queries/votes.sql index a26c64ad1..c7100f268 100644 --- a/vochain/indexer/queries/votes.sql +++ b/vochain/indexer/queries/votes.sql @@ -23,13 +23,17 @@ LIMIT 1; SELECT COUNT(*) FROM votes; -- name: SearchVotes :many -SELECT v.*, t.hash FROM votes AS v -LEFT JOIN transactions AS t - ON v.block_height = t.block_height - AND v.block_index = t.block_index -WHERE (sqlc.arg(process_id) = '' OR process_id = sqlc.arg(process_id)) - AND (sqlc.arg(nullifier_substr) = '' OR (INSTR(LOWER(HEX(nullifier)), sqlc.arg(nullifier_substr)) > 0)) -ORDER BY v.block_height DESC, v.nullifier ASC +WITH results AS ( + SELECT v.*, t.hash + FROM votes AS v + LEFT JOIN transactions AS t + ON v.block_height = t.block_height + AND v.block_index = t.block_index + WHERE (sqlc.arg(process_id) = '' OR process_id = sqlc.arg(process_id)) + AND (sqlc.arg(nullifier_substr) = '' OR (INSTR(LOWER(HEX(nullifier)), sqlc.arg(nullifier_substr)) > 0)) +) +SELECT *, COUNT(*) OVER() AS total_count +FROM results +ORDER BY block_height DESC, nullifier ASC LIMIT sqlc.arg(limit) -OFFSET sqlc.arg(offset) -; +OFFSET sqlc.arg(offset); diff --git a/vochain/indexer/transaction.go b/vochain/indexer/transaction.go index 5d8097886..032f1125f 100644 --- a/vochain/indexer/transaction.go +++ b/vochain/indexer/transaction.go @@ -11,6 +11,7 @@ import ( indexerdb "go.vocdoni.io/dvote/vochain/indexer/db" "go.vocdoni.io/dvote/vochain/indexer/indexertypes" "go.vocdoni.io/dvote/vochain/transaction/vochaintx" + "google.golang.org/protobuf/proto" ) // ErrTransactionNotFound is returned if the transaction is not found. @@ -22,6 +23,11 @@ func (idx *Indexer) CountTotalTransactions() (uint64, error) { return uint64(count), err } +// CountTransactionsByHeight returns the number of transactions indexed for a given height +func (idx *Indexer) CountTransactionsByHeight(height int64) (int64, error) { + return idx.readOnlyQuery.CountTransactionsByHeight(context.TODO(), height) +} + // GetTransaction fetches the txReference for the given tx height func (idx *Indexer) GetTransaction(id uint64) (*indexertypes.Transaction, error) { sqlTxRef, err := idx.readOnlyQuery.GetTransaction(context.TODO(), int64(id)) @@ -63,33 +69,63 @@ func (idx *Indexer) GetTxHashReference(hash types.HexBytes) (*indexertypes.Trans // GetLastTransactions fetches a number of the latest indexed transactions. // The first one returned is the newest, so they are in descending order. -func (idx *Indexer) GetLastTransactions(limit, offset int32) ([]*indexertypes.Transaction, error) { +func (idx *Indexer) GetLastTransactions(limit, offset int32) ([]*indexertypes.Transaction, uint64, error) { sqlTxRefs, err := idx.readOnlyQuery.GetLastTransactions(context.TODO(), indexerdb.GetLastTransactionsParams{ Limit: int64(limit), Offset: int64(offset), }) if err != nil || len(sqlTxRefs) == 0 { if errors.Is(err, sql.ErrNoRows) || len(sqlTxRefs) == 0 { - return nil, ErrTransactionNotFound + return nil, 0, ErrTransactionNotFound } - return nil, fmt.Errorf("could not get last %d tx refs: %v", limit, err) + return nil, 0, fmt.Errorf("could not get last %d tx refs: %v", limit, err) } txRefs := make([]*indexertypes.Transaction, len(sqlTxRefs)) for i, sqlTxRef := range sqlTxRefs { txRefs[i] = indexertypes.TransactionFromDB(&sqlTxRef) } - return txRefs, nil + // TODO: implement TotalCount in GetLastTransactions and return that instead of 0 + return txRefs, 0, nil +} + +// TransactionListByHeight fetches the indexed transactions for a given height. +// The first one returned has TxBlockIndex=0, so they are in ascending order. +func (idx *Indexer) TransactionListByHeight(height, limit, offset int64) ([]*indexertypes.Transaction, uint64, error) { + results, err := idx.readOnlyQuery.TransactionListByHeight(context.TODO(), indexerdb.TransactionListByHeightParams{ + BlockHeight: height, + Limit: limit, + Offset: offset, + }) + if err != nil || len(results) == 0 { + if errors.Is(err, sql.ErrNoRows) || len(results) == 0 { + return nil, 0, ErrTransactionNotFound + } + return nil, 0, fmt.Errorf("could not get %d txs for height %d: %v", limit, height, err) + } + list := make([]*indexertypes.Transaction, len(results)) + for i, row := range results { + list[i] = indexertypes.TransactionFromDBRow(&row) + } + return list, uint64(results[0].TotalCount), nil } func (idx *Indexer) OnNewTx(tx *vochaintx.Tx, blockHeight uint32, txIndex int32) { idx.blockMu.Lock() defer idx.blockMu.Unlock() + + rawtx, err := proto.Marshal(tx.Tx) + if err != nil { + log.Errorw(err, "indexer cannot marshal new transaction") + return + } + queries := idx.blockTxQueries() if _, err := queries.CreateTransaction(context.TODO(), indexerdb.CreateTransactionParams{ Hash: tx.TxID[:], BlockHeight: int64(blockHeight), BlockIndex: int64(txIndex), Type: tx.TxModelType, + RawTx: rawtx, }); err != nil { log.Errorw(err, "cannot index new transaction") } diff --git a/vochain/indexer/vote.go b/vochain/indexer/vote.go index d2c2edcbc..1b6ef36de 100644 --- a/vochain/indexer/vote.go +++ b/vochain/indexer/vote.go @@ -59,19 +59,18 @@ func (idx *Indexer) GetEnvelope(nullifier []byte) (*indexertypes.EnvelopePackage return envelopePackage, nil } -// GetEnvelopes retrieves all envelope metadata for a ProcessId. +// VoteList retrieves all envelope metadata for a ProcessId. // Returns ErrVoteNotFound if the envelope reference is not found. -func (idx *Indexer) GetEnvelopes(processId []byte, max, from int, +func (idx *Indexer) VoteList(processId []byte, max, from int, searchTerm string, -) ([]*indexertypes.EnvelopeMetadata, error) { +) ([]*indexertypes.EnvelopeMetadata, uint64, error) { if from < 0 { - return nil, fmt.Errorf("GetEnvelopes: invalid value: from is invalid value %d", from) + return nil, 0, fmt.Errorf("VoteList: invalid value: from is invalid value %d", from) } if max <= 0 { - return nil, fmt.Errorf("GetEnvelopes: invalid value: max is invalid value %d", max) + return nil, 0, fmt.Errorf("VoteList: invalid value: max is invalid value %d", max) } - envelopes := []*indexertypes.EnvelopeMetadata{} - txRefs, err := idx.readOnlyQuery.SearchVotes(context.TODO(), indexerdb.SearchVotesParams{ + results, err := idx.readOnlyQuery.SearchVotes(context.TODO(), indexerdb.SearchVotesParams{ ProcessID: processId, NullifierSubstr: strings.ToLower(searchTerm), // we search in lowercase Limit: int64(max), @@ -79,11 +78,12 @@ func (idx *Indexer) GetEnvelopes(processId []byte, max, from int, }) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, ErrVoteNotFound + return nil, 0, ErrVoteNotFound } - return nil, err + return nil, 0, err } - for _, txRef := range txRefs { + list := []*indexertypes.EnvelopeMetadata{} + for _, txRef := range results { envelopeMetadata := &indexertypes.EnvelopeMetadata{ ProcessId: txRef.ProcessID, Nullifier: txRef.Nullifier, @@ -92,11 +92,12 @@ func (idx *Indexer) GetEnvelopes(processId []byte, max, from int, TxHash: txRef.Hash, } if len(txRef.VoterID) > 0 { - envelopeMetadata.VoterID = txRef.VoterID.Address() + envelopeMetadata.VoterID = state.VoterID(txRef.VoterID).Address() } - envelopes = append(envelopes, envelopeMetadata) + list = append(list, envelopeMetadata) + } - return envelopes, nil + return list, uint64(results[0].TotalCount), nil } // CountTotalVotes returns the total number of envelopes. diff --git a/vochain/keykeeper/keykeeper.go b/vochain/keykeeper/keykeeper.go index 7f46f654d..4a4425cd1 100644 --- a/vochain/keykeeper/keykeeper.go +++ b/vochain/keykeeper/keykeeper.go @@ -268,9 +268,6 @@ func (*KeyKeeper) OnVote(_ *state.Vote, _ int32) {} // OnNewTx is not used by the KeyKeeper func (*KeyKeeper) OnNewTx(_ *vochaintx.Tx, _ uint32, _ int32) {} -// OnBeginBlock is not used by the KeyKeeper -func (*KeyKeeper) OnBeginBlock(_ state.BeginBlock) {} - // OnCensusUpdate is not used by the KeyKeeper func (*KeyKeeper) OnCensusUpdate(_, _ []byte, _ string, _ uint64) {} diff --git a/vochain/offchaindatahandler/offchaindatahandler.go b/vochain/offchaindatahandler/offchaindatahandler.go index d6e97bd9b..4bc3be6b4 100644 --- a/vochain/offchaindatahandler/offchaindatahandler.go +++ b/vochain/offchaindatahandler/offchaindatahandler.go @@ -166,7 +166,6 @@ func (d *OffChainDataHandler) OnSetAccount(_ []byte, account *state.Account) { func (*OffChainDataHandler) OnCancel(_ []byte, _ int32) {} func (*OffChainDataHandler) OnVote(_ *state.Vote, _ int32) {} func (*OffChainDataHandler) OnNewTx(_ *vochaintx.Tx, _ uint32, _ int32) {} -func (*OffChainDataHandler) OnBeginBlock(state.BeginBlock) {} func (*OffChainDataHandler) OnProcessKeys(_ []byte, _ string, _ int32) {} func (*OffChainDataHandler) OnRevealKeys(_ []byte, _ string, _ int32) {} func (*OffChainDataHandler) OnProcessStatusChange(_ []byte, _ models.ProcessStatus, _ int32) {} diff --git a/vochain/state/eventlistener.go b/vochain/state/eventlistener.go index 1c2d05281..7b599702b 100644 --- a/vochain/state/eventlistener.go +++ b/vochain/state/eventlistener.go @@ -1,8 +1,6 @@ package state import ( - "time" - "go.vocdoni.io/dvote/vochain/transaction/vochaintx" "go.vocdoni.io/proto/build/go/models" ) @@ -32,7 +30,6 @@ type EventListener interface { OnSpendTokens(addr []byte, txType models.TxType, cost uint64, reference string) OnCensusUpdate(pid, censusRoot []byte, censusURI string, censusSize uint64) Commit(height uint32) (err error) - OnBeginBlock(BeginBlock) Rollback() } @@ -46,15 +43,3 @@ func (v *State) AddEventListener(l EventListener) { func (v *State) CleanEventListeners() { v.eventListeners = nil } - -type BeginBlock struct { - Height int64 - Time time.Time - DataHash []byte -} - -func (v *State) OnBeginBlock(bb BeginBlock) { - for _, l := range v.eventListeners { - l.OnBeginBlock(bb) - } -} diff --git a/vochain/state/state_test.go b/vochain/state/state_test.go index e2b76c685..b36dc7722 100644 --- a/vochain/state/state_test.go +++ b/vochain/state/state_test.go @@ -182,7 +182,6 @@ type Listener struct { func (*Listener) OnVote(_ *Vote, _ int32) {} func (*Listener) OnNewTx(_ *vochaintx.Tx, _ uint32, _ int32) {} -func (*Listener) OnBeginBlock(BeginBlock) {} func (*Listener) OnProcess(_ *models.Process, _ int32) {} func (*Listener) OnProcessStatusChange(_ []byte, _ models.ProcessStatus, _ int32) {} func (*Listener) OnProcessDurationChange(_ []byte, _ uint32, _ int32) {}