github.com/DapperCollectives/CAST/backend@v0.0.0-20230921221157-1350c8be7c96/main/server/controllers.go (about)

     1  package server
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"io"
     7  	"net/http"
     8  	"strconv"
     9  
    10  	"github.com/DapperCollectives/CAST/backend/main/models"
    11  	"github.com/DapperCollectives/CAST/backend/main/shared"
    12  	"github.com/gorilla/mux"
    13  	"github.com/rs/zerolog/log"
    14  )
    15  
    16  type errorResponse struct {
    17  	StatusCode int    `json:"statusCode,string"`
    18  	ErrorCode  string `json:"errorCode"`
    19  	Message    string `json:"message"`
    20  	Details    string `json:"details"`
    21  }
    22  
    23  var (
    24  	errIncompleteRequest = errorResponse{
    25  		StatusCode: http.StatusBadRequest,
    26  		ErrorCode:  "ERR_1001",
    27  		Message:    "Error",
    28  		Details:    "There was an error trying to complete your request",
    29  	}
    30  
    31  	errCreateCommunity = errorResponse{
    32  		StatusCode: http.StatusBadRequest,
    33  		ErrorCode:  "ERR_1002",
    34  		Message:    "Error",
    35  		Details:    "There was an error trying to create your community",
    36  	}
    37  
    38  	errFetchingBalance = errorResponse{
    39  		StatusCode: http.StatusBadRequest,
    40  		ErrorCode:  "ERR_1003",
    41  		Message:    "Error Fetching Balance",
    42  		Details: `While confirming your balance, we've encountered an error
    43  							connecting to the Flow Blockchain.`,
    44  	}
    45  
    46  	errInsufficientBalance = errorResponse{
    47  		StatusCode: http.StatusUnauthorized,
    48  		ErrorCode:  "ERR_1004",
    49  		Message:    "Insufficient Balance",
    50  		Details: `In order to vote on this proposal you must have a minimum 
    51  							balance of %f %s tokens in your wallet.`,
    52  	}
    53  
    54  	errForbidden = errorResponse{
    55  		StatusCode: http.StatusForbidden,
    56  		ErrorCode:  "ERR_1005",
    57  		Message:    "Forbidden",
    58  		Details:    "You are not authorized to perform this action.",
    59  	}
    60  
    61  	errCreateProposal = errorResponse{
    62  		StatusCode: http.StatusForbidden,
    63  		ErrorCode:  "ERR_1006",
    64  		Message:    "Error",
    65  		Details:    "There was an error trying to create your proposal",
    66  	}
    67  
    68  	errUpdateCommunity = errorResponse{
    69  		StatusCode: http.StatusForbidden,
    70  		ErrorCode:  "ERR_1007",
    71  		Message:    "Error",
    72  		Details:    "There was an error trying to update your community",
    73  	}
    74  
    75  	errStrategyNotFound = errorResponse{
    76  		StatusCode: http.StatusNotFound,
    77  		ErrorCode:  "ERR_1008",
    78  		Message:    "Strategy Not Found",
    79  		Details:    "The strategy name you are trying to use no longer exists.",
    80  	}
    81  
    82  	errAlreadyVoted = errorResponse{
    83  		StatusCode: http.StatusBadRequest,
    84  		ErrorCode:  "ERR_1009",
    85  		Message:    "Error",
    86  		Details:    "Address %s has already voted for proposal %d.",
    87  	}
    88  
    89  	errInactiveProposal = errorResponse{
    90  		StatusCode: http.StatusBadRequest,
    91  		ErrorCode:  "ERR_1010",
    92  		Message:    "Error",
    93  		Details:    "Cannot vote on an inactive proposal.",
    94  	}
    95  
    96  	errGetCommunity = errorResponse{
    97  		StatusCode: http.StatusInternalServerError,
    98  		ErrorCode:  "ERR_1011",
    99  		Message:    "Error",
   100  		Details:    "There was an error retrieving the community.",
   101  	}
   102  
   103  	errCreateVote = errorResponse{
   104  		StatusCode: http.StatusInternalServerError,
   105  		ErrorCode:  "ERR_1012",
   106  		Message:    "Error",
   107  		Details:    "There was an error creating the vote.",
   108  	}
   109  
   110  	nilErr = errorResponse{}
   111  )
   112  
   113  func (a *App) health(w http.ResponseWriter, r *http.Request) {
   114  	respondWithJSON(w, http.StatusOK, "OK!!")
   115  }
   116  
   117  func (a *App) upload(w http.ResponseWriter, r *http.Request) {
   118  	r.Body = http.MaxBytesReader(w, r.Body, maxFileSize)
   119  	if err := r.ParseMultipartForm(maxFileSize); err != nil {
   120  		log.Error().Err(err).Msgf("File cannot be larger than max file size of %v.\n", maxFileSize)
   121  		respondWithError(w, errIncompleteRequest)
   122  		return
   123  	}
   124  
   125  	resp, err := helpers.uploadFile(r)
   126  	if err != nil {
   127  		log.Error().Err(err).Msg("Error uploading file.")
   128  		respondWithError(w, errIncompleteRequest)
   129  		return
   130  	}
   131  
   132  	respondWithJSON(w, http.StatusOK, resp)
   133  }
   134  
   135  // Votes
   136  func (a *App) getResultsForProposal(w http.ResponseWriter, r *http.Request) {
   137  	vars := mux.Vars(r)
   138  	proposal, err := helpers.fetchProposal(vars, "proposalId")
   139  
   140  	votes, err := models.GetAllVotesForProposal(a.DB, proposal.ID, *proposal.Strategy)
   141  	if err != nil {
   142  		log.Error().Err(err).Msg("Error getting votes for proposal.")
   143  		respondWithError(w, errIncompleteRequest)
   144  		return
   145  	}
   146  
   147  	results, err := helpers.useStrategyTally(proposal, votes)
   148  	if err != nil {
   149  		log.Error().Err(err).Msg("Error tallying votes.")
   150  		respondWithError(w, errIncompleteRequest)
   151  		return
   152  	}
   153  
   154  	if *proposal.Computed_status == "closed" && !proposal.Achievements_done {
   155  		if err := models.AddWinningVoteAchievement(a.DB, votes, results); err != nil {
   156  			log.Error().Err(err).Msg("Error calculating winning votes")
   157  			respondWithError(w, errIncompleteRequest)
   158  		}
   159  	}
   160  
   161  	respondWithJSON(w, http.StatusOK, results)
   162  }
   163  
   164  func (a *App) getVotesForProposal(w http.ResponseWriter, r *http.Request) {
   165  	vars := mux.Vars(r)
   166  	proposal, err := helpers.fetchProposal(vars, "proposalId")
   167  	if err != nil {
   168  		log.Error().Err(err).Msg("Invalid Proposal ID.")
   169  		respondWithError(w, errIncompleteRequest)
   170  		return
   171  	}
   172  
   173  	votes, order, err := helpers.getPaginatedVotes(r, proposal)
   174  	if err != nil {
   175  		log.Error().Err(err).Msg("error getting paginated votes")
   176  		respondWithError(w, errIncompleteRequest)
   177  		return
   178  	}
   179  
   180  	votesWithWeights, err := helpers.useStrategyGetVotes(proposal, votes)
   181  	if err != nil {
   182  		log.Error().Err(err).Msg("error calling useStrategyGetVotes")
   183  		respondWithError(w, errIncompleteRequest)
   184  		return
   185  	}
   186  
   187  	response := shared.GetPaginatedResponseWithPayload(votesWithWeights, order)
   188  	respondWithJSON(w, http.StatusOK, response)
   189  }
   190  
   191  func (a *App) getVoteForAddress(w http.ResponseWriter, r *http.Request) {
   192  	vars := mux.Vars(r)
   193  	addr := vars["addr"]
   194  
   195  	proposal, err := helpers.fetchProposal(vars, "proposalId")
   196  	if err != nil {
   197  		log.Error().Err(err).Msg("Invalid Proposal ID.")
   198  		respondWithError(w, errIncompleteRequest)
   199  		return
   200  	}
   201  
   202  	vote, err := helpers.processVote(addr, proposal)
   203  	if err != nil {
   204  		log.Error().Err(err).Msg("Error processing vote.")
   205  		respondWithError(w, errIncompleteRequest)
   206  		return
   207  	}
   208  
   209  	respondWithJSON(w, http.StatusOK, vote)
   210  }
   211  
   212  func (a *App) getVotesForAddress(w http.ResponseWriter, r *http.Request) {
   213  	var proposalIds []int
   214  
   215  	vars := mux.Vars(r)
   216  	addr := vars["addr"]
   217  
   218  	err := json.Unmarshal([]byte(r.FormValue("proposalIds")), &proposalIds)
   219  	if err != nil {
   220  		log.Error().Err(err).Msg("Error unmarshalling proposalIds")
   221  		respondWithError(w, errIncompleteRequest)
   222  		return
   223  	}
   224  
   225  	pageParams := getPageParams(*r, 25)
   226  
   227  	votes, pageParams, err := helpers.processVotes(addr, proposalIds, pageParams)
   228  	if err != nil {
   229  		log.Error().Err(err).Msg("Error processing votes.")
   230  		respondWithError(w, errIncompleteRequest)
   231  		return
   232  	}
   233  
   234  	response := shared.GetPaginatedResponseWithPayload(votes, pageParams)
   235  	respondWithJSON(w, http.StatusOK, response)
   236  }
   237  
   238  func (a *App) createVoteForProposal(w http.ResponseWriter, r *http.Request) {
   239  	vars := mux.Vars(r)
   240  
   241  	proposal, err := helpers.fetchProposal(vars, "proposalId")
   242  	if err != nil {
   243  		log.Error().Err(err).Msg("Invalid Proposal ID.")
   244  		respondWithError(w, errIncompleteRequest)
   245  		return
   246  	}
   247  
   248  	vote, errResponse := helpers.createVote(r, proposal)
   249  	if errResponse != nilErr {
   250  		log.Error().Err(err).Msg("Error creating vote.")
   251  		respondWithError(w, errResponse)
   252  		return
   253  	}
   254  
   255  	respondWithJSON(w, http.StatusCreated, vote)
   256  }
   257  
   258  // Proposals
   259  func (a *App) getProposalsForCommunity(w http.ResponseWriter, r *http.Request) {
   260  	vars := mux.Vars(r)
   261  	communityId, err := strconv.Atoi(vars["communityId"])
   262  
   263  	if err != nil {
   264  		log.Error().Err(err).Msg("Invalid Community ID")
   265  		respondWithError(w, errIncompleteRequest)
   266  		return
   267  	}
   268  
   269  	pageParams := getPageParams(*r, 25)
   270  	status := r.FormValue("status")
   271  
   272  	proposals, totalRecords, err := models.GetProposalsForCommunity(
   273  		a.DB,
   274  		communityId,
   275  		status,
   276  		pageParams,
   277  	)
   278  	if err != nil {
   279  		log.Error().Err(err).Msg("Error getting proposals for community.")
   280  		respondWithError(w, errIncompleteRequest)
   281  		return
   282  	}
   283  
   284  	pageParams.TotalRecords = totalRecords
   285  
   286  	response := shared.GetPaginatedResponseWithPayload(proposals, pageParams)
   287  	respondWithJSON(w, http.StatusOK, response)
   288  }
   289  
   290  func (a *App) getProposal(w http.ResponseWriter, r *http.Request) {
   291  	vars := mux.Vars(r)
   292  	p, err := helpers.fetchProposal(vars, "id")
   293  	if err != nil {
   294  		log.Error().Err(err).Msg("Invalid Proposal ID.")
   295  		respondWithError(w, errIncompleteRequest)
   296  		return
   297  	}
   298  
   299  	c, err := helpers.fetchCommunity(p.Community_id)
   300  	if err != nil {
   301  		log.Error().Err(err).Msg("error fetching community")
   302  		respondWithError(w, errIncompleteRequest)
   303  		return
   304  	}
   305  
   306  	_, err = models.MatchStrategyByProposal(*c.Strategies, *p.Strategy)
   307  	if err != nil {
   308  		log.Error().Err(err).Msg("error getting strategy by proposal")
   309  		respondWithError(w, errIncompleteRequest)
   310  		return
   311  	}
   312  
   313  	respondWithJSON(w, http.StatusOK, p)
   314  }
   315  
   316  func (a *App) createProposal(w http.ResponseWriter, r *http.Request) {
   317  	vars := mux.Vars(r)
   318  	communityId, err := strconv.Atoi(vars["communityId"])
   319  	if err != nil {
   320  		log.Error().Err(err).Msg("Invalid Community ID")
   321  		respondWithError(w, errIncompleteRequest)
   322  		return
   323  	}
   324  
   325  	var p models.Proposal
   326  	p.Community_id = communityId
   327  
   328  	if err := validatePayload(r.Body, &p); err != nil {
   329  		log.Error().Err(err).Msg("Error validating payload")
   330  		respondWithError(w, errIncompleteRequest)
   331  		return
   332  	}
   333  
   334  	proposal, errResponse := helpers.createProposal(p)
   335  	if errResponse != nilErr {
   336  		log.Error().Err(err).Msg("Error creating proposal")
   337  		respondWithError(w, errResponse)
   338  		return
   339  	}
   340  
   341  	respondWithJSON(w, http.StatusCreated, proposal)
   342  }
   343  
   344  func (a *App) updateProposal(w http.ResponseWriter, r *http.Request) {
   345  	vars := mux.Vars(r)
   346  	p, err := helpers.fetchProposal(vars, "id")
   347  	if err != nil {
   348  		log.Error().Err(err).Msg("Invalid Proposal ID.")
   349  		respondWithError(w, errIncompleteRequest)
   350  		return
   351  	}
   352  
   353  	var payload models.UpdateProposalRequestPayload
   354  	if err := validatePayload(r.Body, &payload); err != nil {
   355  		log.Error().Err(err).Msg("Error validating payload")
   356  		respondWithError(w, errIncompleteRequest)
   357  		return
   358  	}
   359  
   360  	// Check that status update is valid
   361  	// For now we are assuming proposals are creating with
   362  	// status 'published' and may be cancelled.
   363  	if payload.Status != "cancelled" {
   364  		log.Error().Err(err).Msg("Invalid status update")
   365  		respondWithError(w, errIncompleteRequest)
   366  		return
   367  	}
   368  
   369  	if payload.Voucher != nil {
   370  		if err := helpers.validateUserWithRoleViaVoucher(
   371  			payload.Signing_addr,
   372  			payload.Voucher,
   373  			p.Community_id,
   374  			"author"); err != nil {
   375  			log.Error().Err(err).Msg("Error validating user with role via voucher")
   376  			respondWithError(w, errForbidden)
   377  			return
   378  		}
   379  	} else {
   380  		if err := helpers.validateUserWithRole(
   381  			payload.Signing_addr,
   382  			payload.Timestamp,
   383  			payload.Composite_signatures,
   384  			p.Community_id,
   385  			"author"); err != nil {
   386  			log.Error().Err(err).Msg("Error validating user with role")
   387  			respondWithError(w, errForbidden)
   388  			return
   389  		}
   390  	}
   391  
   392  	p.Status = &payload.Status
   393  	p.Cid, err = helpers.pinJSONToIpfs(p)
   394  	if err != nil {
   395  		log.Error().Err(err).Msg("Error pinning proposal to IPFS")
   396  		respondWithError(w, errIncompleteRequest)
   397  		return
   398  	}
   399  
   400  	if err := p.UpdateProposal(a.DB); err != nil {
   401  		log.Error().Err(err).Msg("Error updating proposal")
   402  		respondWithError(w, errIncompleteRequest)
   403  		return
   404  	}
   405  
   406  	respondWithJSON(w, http.StatusOK, p)
   407  }
   408  
   409  // Communities
   410  func (a *App) getCommunities(w http.ResponseWriter, r *http.Request) {
   411  	pageParams := getPageParams(*r, 25)
   412  
   413  	communities, totalRecords, err := models.GetCommunities(a.DB, pageParams)
   414  	if err != nil {
   415  		log.Error().Err(err).Msg("Error fetching communities")
   416  		respondWithError(w, errIncompleteRequest)
   417  		return
   418  	}
   419  
   420  	pageParams.TotalRecords = totalRecords
   421  	response := shared.GetPaginatedResponseWithPayload(communities, pageParams)
   422  
   423  	respondWithJSON(w, http.StatusOK, response)
   424  }
   425  
   426  func (a *App) searchCommunities(w http.ResponseWriter, r *http.Request) {
   427  	pageParams := getPageParams(*r, 25)
   428  	filters := r.FormValue("filters")
   429  	searchText := r.FormValue("text")
   430  
   431  	results, totalRecords, categories, err := helpers.searchCommunities(
   432  		searchText,
   433  		filters,
   434  		pageParams,
   435  	)
   436  	if err != nil {
   437  		log.Error().Err(err).Msg("Error searching communities")
   438  		respondWithError(w, errIncompleteRequest)
   439  	}
   440  
   441  	pageParams.TotalRecords = totalRecords
   442  
   443  	paginatedResults, err := helpers.appendFiltersToResponse(
   444  		results,
   445  		pageParams,
   446  		categories,
   447  	)
   448  	if err != nil {
   449  		log.Error().Err(err).Msg("Error appending filters to response")
   450  		respondWithError(w, errIncompleteRequest)
   451  	}
   452  
   453  	respondWithJSON(w, http.StatusOK, paginatedResults)
   454  }
   455  
   456  func (a *App) getCommunity(w http.ResponseWriter, r *http.Request) {
   457  	vars := mux.Vars(r)
   458  	id, err := strconv.Atoi(vars["id"])
   459  	if err != nil {
   460  		log.Error().Err(err).Msg("Invalid Community ID")
   461  		respondWithError(w, errIncompleteRequest)
   462  		return
   463  	}
   464  
   465  	c, err := helpers.fetchCommunity(id)
   466  	if err != nil {
   467  		log.Error().Err(err).Msg("Error fetching community")
   468  		respondWithError(w, errIncompleteRequest)
   469  		return
   470  	}
   471  
   472  	respondWithJSON(w, http.StatusOK, c)
   473  }
   474  
   475  func (a *App) getCommunitiesForHomePage(w http.ResponseWriter, r *http.Request) {
   476  	pageParams := getPageParams(*r, 25)
   477  	isSearch := false
   478  
   479  	communities, totalRecords, err := models.GetDefaultCommunities(
   480  		a.DB,
   481  		pageParams,
   482  		[]string{},
   483  		isSearch,
   484  	)
   485  	if err != nil {
   486  		log.Error().Err(err).Msg("Error fetching communities for home page")
   487  		respondWithError(w, errIncompleteRequest)
   488  		return
   489  	}
   490  
   491  	pageParams.TotalRecords = totalRecords
   492  
   493  	response := shared.GetPaginatedResponseWithPayload(communities, pageParams)
   494  	respondWithJSON(w, http.StatusOK, response)
   495  }
   496  
   497  func (a *App) createCommunity(w http.ResponseWriter, r *http.Request) {
   498  	var err error
   499  	var c models.Community
   500  	var payload models.CreateCommunityRequestPayload
   501  
   502  	if err := validatePayload(r.Body, &payload); err != nil {
   503  		log.Error().Err(err).Msg("Error validating payload")
   504  		respondWithError(w, errIncompleteRequest)
   505  		return
   506  	}
   507  
   508  	//Validate Strategies & Proposal Thresholds
   509  	if payload.Strategies != nil {
   510  		err = validateContractThreshold(*payload.Strategies)
   511  		if err != nil {
   512  			log.Error().Err(err).Msg("Error validating contract threshold")
   513  			respondWithError(w, errIncompleteRequest)
   514  			return
   515  		}
   516  	}
   517  	if payload.Proposal_threshold != nil && payload.Only_authors_to_submit != nil {
   518  		err = validateProposalThreshold(*payload.Proposal_threshold, *payload.Only_authors_to_submit)
   519  		if err != nil {
   520  			log.Error().Err(err).Msg("Error validating proposal threshold")
   521  			respondWithError(w, errIncompleteRequest)
   522  		}
   523  	}
   524  
   525  	c, err = helpers.createCommunity(payload)
   526  	if err != nil {
   527  		log.Error().Err(err).Msg("Error creating community")
   528  		respondWithError(w, errIncompleteRequest)
   529  		return
   530  	}
   531  
   532  	respondWithJSON(w, http.StatusCreated, c)
   533  }
   534  
   535  func (a *App) updateCommunity(w http.ResponseWriter, r *http.Request) {
   536  	vars := mux.Vars(r)
   537  	id, err := strconv.Atoi(vars["id"])
   538  	if err != nil {
   539  		log.Error().Err(err).Msg("Invalid Community ID")
   540  		respondWithError(w, errIncompleteRequest)
   541  		return
   542  	}
   543  	var payload models.UpdateCommunityRequestPayload
   544  
   545  	if err := validatePayload(r.Body, &payload); err != nil {
   546  		log.Error().Err(err).Msg("Error validating payload")
   547  		respondWithError(w, errIncompleteRequest)
   548  		return
   549  	}
   550  
   551  	//Validate Contract Thresholds
   552  	if payload.Strategies != nil {
   553  		err = validateContractThreshold(*payload.Strategies)
   554  		if err != nil {
   555  			log.Error().Err(err).Msg("Error validating contract threshold")
   556  			respondWithError(w, errIncompleteRequest)
   557  			return
   558  		}
   559  	}
   560  
   561  	if payload.Proposal_threshold != nil && payload.Only_authors_to_submit != nil {
   562  		err = validateProposalThreshold(*payload.Proposal_threshold, *payload.Only_authors_to_submit)
   563  		if err != nil {
   564  			log.Error().Err(err).Msg("Error validating proposal threshold")
   565  			respondWithError(w, errIncompleteRequest)
   566  		}
   567  	}
   568  
   569  	c, err := helpers.updateCommunity(id, payload)
   570  	if err != nil {
   571  		log.Error().Err(err).Msg("Error updating community")
   572  		respondWithError(w, errIncompleteRequest)
   573  		return
   574  	}
   575  
   576  	respondWithJSON(w, http.StatusOK, c)
   577  }
   578  
   579  func validateConractThreshold(s []models.Strategy) error {
   580  	for _, s := range s {
   581  		if s.Threshold != nil {
   582  			if *s.Threshold < 1 {
   583  				return errors.New("Contract Threshold Cannot Be < 1.")
   584  			}
   585  		}
   586  	}
   587  	return nil
   588  }
   589  
   590  // Voting Strategies
   591  func (a *App) getVotingStrategies(w http.ResponseWriter, r *http.Request) {
   592  	vs, err := models.GetVotingStrategies(a.DB)
   593  
   594  	// Add custom scripts for the custom-script strategy
   595  	for _, strategy := range vs {
   596  		if strategy.Key == "custom-script" {
   597  			strategy.Scripts = customScripts
   598  		}
   599  	}
   600  
   601  	if err != nil {
   602  		log.Error().Err(err).Msg("Error fetching voting strategies")
   603  		respondWithError(w, errIncompleteRequest)
   604  		return
   605  	}
   606  
   607  	respondWithJSON(w, http.StatusOK, vs)
   608  }
   609  
   610  func (a *App) getCommunityCategories(w http.ResponseWriter, r *http.Request) {
   611  	vs, err := models.GetCommunityTypes(a.DB)
   612  	if err != nil {
   613  		log.Error().Err(err).Msg("Error fetching community categories")
   614  		respondWithError(w, errIncompleteRequest)
   615  		return
   616  	}
   617  
   618  	respondWithJSON(w, http.StatusOK, vs)
   619  }
   620  
   621  func (a *App) getActiveStrategiesForCommunity(w http.ResponseWriter, r *http.Request) {
   622  	vars := mux.Vars(r)
   623  	communityId, err := strconv.Atoi(vars["communityId"])
   624  
   625  	if err != nil {
   626  		log.Error().Err(err).Msg("Invalid Community ID")
   627  		respondWithError(w, errIncompleteRequest)
   628  		return
   629  	}
   630  
   631  	strategies, err := models.GetActiveStrategiesForCommunity(a.DB, communityId)
   632  	if err != nil {
   633  		log.Error().Err(err).Msg("Error fetching active strategies for community")
   634  		respondWithError(w, errIncompleteRequest)
   635  		return
   636  	}
   637  
   638  	respondWithJSON(w, http.StatusOK, strategies)
   639  }
   640  
   641  ////////////
   642  // Lists //
   643  ///////////
   644  
   645  func (a *App) getListsForCommunity(w http.ResponseWriter, r *http.Request) {
   646  	vars := mux.Vars(r)
   647  	communityId, err := strconv.Atoi(vars["communityId"])
   648  	if err != nil {
   649  		log.Error().Err(err).Msg("Invalid Community ID")
   650  		respondWithError(w, errIncompleteRequest)
   651  		return
   652  	}
   653  
   654  	lists, err := models.GetListsForCommunity(a.DB, communityId)
   655  	if err != nil {
   656  		log.Error().Err(err).Msg("Error getting lists for community")
   657  		respondWithError(w, errIncompleteRequest)
   658  		return
   659  	}
   660  
   661  	respondWithJSON(w, http.StatusOK, lists)
   662  }
   663  
   664  func (a *App) getList(w http.ResponseWriter, r *http.Request) {
   665  	vars := mux.Vars(r)
   666  	id, err := strconv.Atoi(vars["id"])
   667  
   668  	if err != nil {
   669  		log.Error().Err(err).Msg("Invalid List ID")
   670  		respondWithError(w, errIncompleteRequest)
   671  		return
   672  	}
   673  	list := models.List{ID: id}
   674  
   675  	if err = list.GetListById(a.DB); err != nil {
   676  		log.Error().Err(err).Msg("Error getting list")
   677  		respondWithError(w, errIncompleteRequest)
   678  		return
   679  	}
   680  
   681  	respondWithJSON(w, http.StatusOK, list)
   682  }
   683  
   684  func (a *App) createListForCommunity(w http.ResponseWriter, r *http.Request) {
   685  	vars := mux.Vars(r)
   686  	communityId, err := strconv.Atoi(vars["communityId"])
   687  	if err != nil {
   688  		log.Error().Err(err).Msg("Invalid Community ID")
   689  		respondWithError(w, errIncompleteRequest)
   690  		return
   691  	}
   692  
   693  	payload := models.ListPayload{}
   694  	payload.Community_id = communityId
   695  
   696  	if err := validatePayload(r.Body, &payload); err != nil {
   697  		log.Error().Err(err).Msg("Error validating payload")
   698  		respondWithError(w, errIncompleteRequest)
   699  		return
   700  	}
   701  
   702  	l, httpStatus, err := helpers.createListForCommunity(payload)
   703  	if err != nil {
   704  		log.Error().Err(err).Msg("Error creating list for community")
   705  		errIncompleteRequest.StatusCode = httpStatus
   706  		respondWithError(w, errIncompleteRequest)
   707  		return
   708  	}
   709  
   710  	respondWithJSON(w, http.StatusCreated, l)
   711  }
   712  
   713  func (a *App) addAddressesToList(w http.ResponseWriter, r *http.Request) {
   714  	vars := mux.Vars(r)
   715  	id, err := strconv.Atoi(vars["id"])
   716  	if err != nil {
   717  		log.Error().Err(err).Msg("Invalid List ID")
   718  		respondWithError(w, errIncompleteRequest)
   719  		return
   720  	}
   721  
   722  	payload := models.ListUpdatePayload{}
   723  	if err := validatePayload(r.Body, &payload); err != nil {
   724  		log.Error().Err(err).Msg("Error validating payload")
   725  		respondWithError(w, errIncompleteRequest)
   726  		return
   727  	}
   728  
   729  	httpStatus, err := helpers.updateAddressesInList(id, payload, "add")
   730  	if err != nil {
   731  		log.Error().Err(err).Msg("Error adding addresses to list")
   732  		errIncompleteRequest.StatusCode = httpStatus
   733  		respondWithError(w, errCreateCommunity)
   734  		return
   735  	}
   736  
   737  	respondWithJSON(w, http.StatusCreated, "OK")
   738  }
   739  
   740  func (a *App) removeAddressesFromList(w http.ResponseWriter, r *http.Request) {
   741  	vars := mux.Vars(r)
   742  	id, err := strconv.Atoi(vars["id"])
   743  	if err != nil {
   744  		log.Error().Err(err).Msg("Invalid List ID")
   745  		respondWithError(w, errIncompleteRequest)
   746  		return
   747  	}
   748  
   749  	payload := models.ListUpdatePayload{}
   750  	if err := validatePayload(r.Body, &payload); err != nil {
   751  		log.Error().Err(err).Msg("Error validating payload")
   752  		respondWithError(w, errIncompleteRequest)
   753  		return
   754  	}
   755  
   756  	httpStatus, err := helpers.updateAddressesInList(id, payload, "remove")
   757  	if err != nil {
   758  		log.Error().Err(err).Msg("Error removing addresses from list")
   759  		errIncompleteRequest.StatusCode = httpStatus
   760  		respondWithError(w, errIncompleteRequest)
   761  		return
   762  	}
   763  
   764  	respondWithJSON(w, http.StatusOK, "OK")
   765  }
   766  
   767  //////////////
   768  // Accounts //
   769  //////////////
   770  
   771  func (a *App) getAccountAtBlockHeight(w http.ResponseWriter, r *http.Request) {
   772  	vars := mux.Vars(r)
   773  	addr := vars["addr"]
   774  	var blockHeight uint64
   775  	blockHeight, err := strconv.ParseUint(vars["blockHeight"], 10, 64)
   776  	if err != nil {
   777  		log.Error().Err(err).Msg("Error parsing blockHeight param.")
   778  		respondWithError(w, errFetchingBalance)
   779  		return
   780  	}
   781  
   782  	flowToken := "FlowToken"
   783  
   784  	b := shared.FTBalanceResponse{}
   785  	acc, err := a.FlowAdapter.GetAccountAtBlockHeight(addr, blockHeight)
   786  	if err != nil {
   787  		log.Error().Err(err).Msgf("Error getting account %s at blockheight %d.", addr, blockHeight)
   788  	}
   789  
   790  	//TODO: @bluesign add locked tokens
   791  	b.Balance = acc.Balance
   792  	b.Addr = addr
   793  	b.BlockHeight = blockHeight
   794  	b.FungibleTokenID = flowToken
   795  
   796  	respondWithJSON(w, http.StatusOK, b)
   797  }
   798  
   799  func (a *App) getAdminList(w http.ResponseWriter, r *http.Request) {
   800  	respondWithJSON(w, http.StatusOK, a.AdminAllowlist.Addresses)
   801  }
   802  
   803  func (a *App) getCommunityBlocklist(w http.ResponseWriter, r *http.Request) {
   804  	respondWithJSON(w, http.StatusOK, a.CommunityBlocklist.Addresses)
   805  }
   806  
   807  ///////////
   808  // Users //
   809  ///////////
   810  
   811  func (a *App) createCommunityUser(w http.ResponseWriter, r *http.Request) {
   812  	vars := mux.Vars(r)
   813  	communityId, err := strconv.Atoi(vars["communityId"])
   814  	if err != nil {
   815  		log.Error().Err(err).Msg("Invalid Community ID")
   816  		respondWithError(w, errIncompleteRequest)
   817  		return
   818  	}
   819  
   820  	payload := models.CommunityUserPayload{}
   821  	payload.Community_id = communityId
   822  
   823  	if err := validatePayload(r.Body, &payload); err != nil {
   824  		log.Error().Err(err).Msg("Error validating payload")
   825  		respondWithError(w, errIncompleteRequest)
   826  		return
   827  	}
   828  
   829  	httpStatus, err := helpers.createCommunityUser(payload)
   830  	if err != nil {
   831  		log.Error().Err(err).Msg("Error creating community user")
   832  		errCreateCommunity.StatusCode = httpStatus
   833  		respondWithError(w, errCreateCommunity)
   834  		return
   835  	}
   836  
   837  	respondWithJSON(w, http.StatusCreated, "OK")
   838  }
   839  
   840  func (a *App) getCommunityUsers(w http.ResponseWriter, r *http.Request) {
   841  	vars := mux.Vars(r)
   842  	communityId, err := strconv.Atoi(vars["communityId"])
   843  
   844  	if err != nil {
   845  		log.Error().Err(err).Msg("Invalid Community ID")
   846  		respondWithError(w, errIncompleteRequest)
   847  		return
   848  	}
   849  
   850  	pageParams := getPageParams(*r, 100)
   851  
   852  	users, totalRecords, err := models.GetUsersForCommunity(a.DB, communityId, pageParams)
   853  	if err != nil {
   854  		log.Error().Err(err).Msg("Error getting community users")
   855  		respondWithError(w, errIncompleteRequest)
   856  		return
   857  	}
   858  
   859  	pageParams.TotalRecords = totalRecords
   860  
   861  	response := shared.GetPaginatedResponseWithPayload(users, pageParams)
   862  	respondWithJSON(w, http.StatusOK, response)
   863  
   864  }
   865  
   866  func (a *App) getCommunityUsersByType(w http.ResponseWriter, r *http.Request) {
   867  	vars := mux.Vars(r)
   868  	communityId, err := strconv.Atoi(vars["communityId"])
   869  
   870  	if err != nil {
   871  		log.Error().Err(err).Msg("Invalid Community ID")
   872  		respondWithError(w, errIncompleteRequest)
   873  		return
   874  	}
   875  
   876  	userType := vars["userType"]
   877  	if !models.EnsureValidRole(userType) {
   878  		log.Error().Err(err).Msg("Invalid User Type")
   879  		respondWithError(w, errIncompleteRequest)
   880  		return
   881  	}
   882  
   883  	pageParams := getPageParams(*r, 100)
   884  	users, totalRecords, err := models.GetUsersForCommunityByType(
   885  		a.DB,
   886  		communityId,
   887  		userType,
   888  		pageParams,
   889  	)
   890  	if err != nil {
   891  		log.Error().Err(err).Msg("Error getting community users")
   892  		respondWithError(w, errIncompleteRequest)
   893  		return
   894  	}
   895  	pageParams.TotalRecords = totalRecords
   896  
   897  	response := shared.GetPaginatedResponseWithPayload(users, pageParams)
   898  	respondWithJSON(w, http.StatusOK, response)
   899  }
   900  
   901  func (a *App) getCommunityLeaderboard(w http.ResponseWriter, r *http.Request) {
   902  	vars := mux.Vars(r)
   903  	communityId, err := strconv.Atoi(vars["communityId"])
   904  
   905  	if err != nil {
   906  		log.Error().Err(err).Msg("Invalid Community ID")
   907  		respondWithError(w, errIncompleteRequest)
   908  		return
   909  	}
   910  
   911  	addr := r.FormValue("addr")
   912  	pageParams := getPageParams(*r, 100)
   913  
   914  	leaderboard, totalRecords, err := models.GetCommunityLeaderboard(a.DB, communityId, addr, pageParams)
   915  	if err != nil {
   916  		log.Error().Err(err).Msg("Error getting community leaderboard")
   917  		respondWithError(w, errIncompleteRequest)
   918  		return
   919  	}
   920  	pageParams.TotalRecords = totalRecords
   921  
   922  	response := shared.GetPaginatedResponseWithPayload(leaderboard.Users, pageParams)
   923  	response.Data = leaderboard
   924  
   925  	respondWithJSON(w, http.StatusOK, response)
   926  }
   927  
   928  func (a *App) getUserCommunities(w http.ResponseWriter, r *http.Request) {
   929  	vars := mux.Vars(r)
   930  	addr := vars["addr"]
   931  
   932  	pageParams := getPageParams(*r, 100)
   933  
   934  	communities, totalRecords, err := models.GetCommunitiesForUser(a.DB, addr, pageParams)
   935  	if err != nil {
   936  		log.Error().Err(err).Msg("Error getting user communities")
   937  		respondWithError(w, errIncompleteRequest)
   938  		return
   939  	}
   940  
   941  	pageParams.TotalRecords = totalRecords
   942  	response := shared.GetPaginatedResponseWithPayload(communities, pageParams)
   943  
   944  	respondWithJSON(w, http.StatusOK, response)
   945  
   946  }
   947  
   948  func (a *App) removeUserRole(w http.ResponseWriter, r *http.Request) {
   949  	vars := mux.Vars(r)
   950  	addr := vars["addr"]
   951  	userType := vars["userType"]
   952  	communityId, err := strconv.Atoi(vars["communityId"])
   953  
   954  	if err != nil {
   955  		log.Error().Err(err).Msg("Invalid Community ID")
   956  		respondWithError(w, errIncompleteRequest)
   957  		return
   958  	}
   959  
   960  	payload := models.CommunityUserPayload{}
   961  	payload.Community_id = communityId
   962  	payload.Addr = addr
   963  	payload.User_type = userType
   964  
   965  	if err := validatePayload(r.Body, &payload); err != nil {
   966  		log.Error().Err(err).Msg("Error validating payload")
   967  		respondWithError(w, errIncompleteRequest)
   968  		return
   969  	}
   970  
   971  	_, err = helpers.removeUserRole(payload)
   972  	if err != nil {
   973  		log.Error().Err(err).Msg("Error removing user role")
   974  		respondWithError(w, errIncompleteRequest)
   975  		return
   976  	}
   977  
   978  	respondWithJSON(w, http.StatusOK, "OK")
   979  }
   980  
   981  /////////////
   982  // HELPERS //
   983  /////////////
   984  
   985  func respondWithError(w http.ResponseWriter, err errorResponse) {
   986  	respondWithJSON(w, err.StatusCode, map[string]string{
   987  		"statusCode": strconv.Itoa(err.StatusCode),
   988  		"errorCode":  err.ErrorCode,
   989  		"message":    err.Message,
   990  		"details":    err.Details,
   991  	})
   992  }
   993  
   994  func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
   995  	response, _ := json.Marshal(payload)
   996  	w.Header().Set("Content-Type", "application/json")
   997  	w.WriteHeader(code)
   998  	w.Write(response)
   999  }
  1000  
  1001  func validatePayload(body io.ReadCloser, data interface{}) error {
  1002  	decoder := json.NewDecoder(body)
  1003  	if err := decoder.Decode(&data); err != nil {
  1004  		errMsg := "Invalid request payload."
  1005  		log.Error().Err(err).Msg(errMsg)
  1006  		return errors.New(errMsg)
  1007  	}
  1008  
  1009  	defer body.Close()
  1010  
  1011  	return nil
  1012  }
  1013  
  1014  func getPageParams(r http.Request, defaultCount int) shared.PageParams {
  1015  	s, _ := strconv.Atoi(r.FormValue("start"))
  1016  	c, _ := strconv.Atoi(r.FormValue("count"))
  1017  	o := r.FormValue("order")
  1018  
  1019  	if o == "" {
  1020  		o = "desc"
  1021  	}
  1022  
  1023  	if c > defaultCount || c < 1 {
  1024  		c = defaultCount
  1025  	}
  1026  	if s < 0 {
  1027  		s = 0
  1028  	}
  1029  
  1030  	return shared.PageParams{
  1031  		Start: s,
  1032  		Count: c,
  1033  		Order: o,
  1034  	}
  1035  }