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

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"net/http"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/DapperCollectives/CAST/backend/main/models"
    16  	"github.com/DapperCollectives/CAST/backend/main/shared"
    17  	"github.com/go-playground/validator/v10"
    18  	"github.com/jackc/pgx/v4"
    19  	"github.com/rs/zerolog/log"
    20  	"github.com/thoas/go-funk"
    21  )
    22  
    23  var allowedFileTypes = []string{"image/jpg", "image/jpeg", "image/png", "image/gif"}
    24  
    25  const (
    26  	maxFileSize = 5 * 1024 * 1024 // 5MB
    27  )
    28  
    29  type Helpers struct {
    30  	A *App
    31  }
    32  
    33  func (h *Helpers) Initialize(app *App) {
    34  	h.A = app
    35  }
    36  
    37  func (h *Helpers) useStrategyTally(
    38  	p models.Proposal,
    39  	v []*models.VoteWithBalance,
    40  ) (models.ProposalResults, error) {
    41  
    42  	s := h.initStrategy(*p.Strategy)
    43  	if s == nil {
    44  		return models.ProposalResults{}, errors.New("Strategy not found.")
    45  	}
    46  
    47  	proposalInitialized := models.NewProposalResults(p.ID, p.Choices)
    48  	results, err := s.TallyVotes(v, proposalInitialized, &p)
    49  	if err != nil {
    50  		return models.ProposalResults{}, err
    51  	}
    52  
    53  	return results, nil
    54  }
    55  
    56  func (h *Helpers) useStrategyGetVotes(
    57  	p models.Proposal,
    58  	v []*models.VoteWithBalance,
    59  ) ([]*models.VoteWithBalance, error) {
    60  
    61  	s := h.initStrategy(*p.Strategy)
    62  	if s == nil {
    63  		return nil, errors.New("Strategy not found.")
    64  	}
    65  
    66  	votesWithWeights, err := s.GetVotes(v, &p)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	return votesWithWeights, nil
    72  }
    73  
    74  func (h *Helpers) useStrategyGetVoteWeight(
    75  	p models.Proposal,
    76  	v *models.VoteWithBalance,
    77  ) (float64, error) {
    78  	s := h.initStrategy(*p.Strategy)
    79  	if s == nil {
    80  		return 0, errors.New("Strategy not found.")
    81  	}
    82  
    83  	weight, err := s.GetVoteWeightForBalance(v, &p)
    84  	if err != nil {
    85  		return 0, err
    86  	}
    87  
    88  	return weight, nil
    89  }
    90  
    91  func (h *Helpers) useStrategyFetchBalance(
    92  	v models.Vote,
    93  	p models.Proposal,
    94  	s Strategy,
    95  ) (models.VoteWithBalance, errorResponse) {
    96  
    97  	emptyBalance := &models.Balance{
    98  		Addr:        v.Addr,
    99  		Proposal_id: p.ID,
   100  	}
   101  
   102  	if p.Block_height != nil {
   103  		emptyBalance.BlockHeight = *p.Block_height
   104  	}
   105  
   106  	c := models.Community{ID: p.Community_id}
   107  	if err := c.GetCommunityByProposalId(h.A.DB, p.ID); err != nil {
   108  		return models.VoteWithBalance{}, errGetCommunity
   109  	}
   110  
   111  	strategy, err := c.GetStrategy(*p.Strategy)
   112  	if err != nil {
   113  		return models.VoteWithBalance{}, errStrategyNotFound
   114  	}
   115  
   116  	balance, err := s.FetchBalance(emptyBalance, &p)
   117  	if err != nil {
   118  		log.Error().Err(err).Msgf("User does not have the required balance %v.", v.Addr)
   119  		errResponse := errInsufficientBalance
   120  		errResponse.Details = fmt.Sprintf(errResponse.Details, *strategy.Threshold, *strategy.Contract.Name)
   121  		return models.VoteWithBalance{}, errResponse
   122  	}
   123  
   124  	vb := models.VoteWithBalance{
   125  		Vote:                    v,
   126  		PrimaryAccountBalance:   &balance.PrimaryAccountBalance,
   127  		SecondaryAccountBalance: &balance.SecondaryAccountBalance,
   128  		StakingBalance:          &balance.StakingBalance,
   129  	}
   130  
   131  	return vb, nilErr
   132  }
   133  
   134  func (h *Helpers) fetchProposal(vars map[string]string, query string) (models.Proposal, error) {
   135  	proposalId, err := strconv.Atoi(vars[query])
   136  	if err != nil {
   137  		msg := fmt.Sprintf("Invalid proposalId: %s", vars["proposalId"])
   138  		log.Error().Err(err).Msg(msg)
   139  		return models.Proposal{}, errors.New(msg)
   140  	}
   141  
   142  	p := models.Proposal{ID: proposalId}
   143  
   144  	if err := p.GetProposalById(h.A.DB); err != nil {
   145  		switch err.Error() {
   146  		case pgx.ErrNoRows.Error():
   147  			msg := fmt.Sprintf("Proposal with ID %d not found.", proposalId)
   148  			return models.Proposal{}, errors.New(msg)
   149  		default:
   150  			return models.Proposal{}, err
   151  		}
   152  	}
   153  
   154  	return p, nil
   155  }
   156  
   157  func (h *Helpers) uploadFile(r *http.Request) (interface{}, error) {
   158  	file, handler, err := r.FormFile("file")
   159  	if err != nil {
   160  		log.Error().Err(err).Msg("FormFile Retrieval Error.")
   161  		return nil, err
   162  	}
   163  	defer file.Close()
   164  
   165  	// ensure mime type is allowed
   166  	mime := handler.Header.Get("Content-Type")
   167  	if !funk.Contains(allowedFileTypes, mime) {
   168  		msg := fmt.Sprintf("Uploaded file type of '%s' is not allowed.", mime)
   169  		log.Error().Msg(msg)
   170  		return nil, errors.New(msg)
   171  	}
   172  
   173  	pin, err := h.A.IpfsClient.PinFile(file, handler.Filename)
   174  	if err != nil {
   175  		log.Error().Err(err).Msg("Error pinning file to IPFS.")
   176  		return nil, err
   177  	}
   178  
   179  	resp := struct {
   180  		Cid string `json:"cid"`
   181  	}{
   182  		Cid: pin.IpfsHash,
   183  	}
   184  
   185  	return resp, nil
   186  }
   187  
   188  func (h *Helpers) getPaginatedVotes(
   189  	r *http.Request,
   190  	p models.Proposal,
   191  ) (
   192  	[]*models.VoteWithBalance,
   193  	shared.PageParams,
   194  	error,
   195  ) {
   196  
   197  	pageParams := getPageParams(*r, 25)
   198  
   199  	votes, totalRecords, err := models.GetVotesForProposal(
   200  		h.A.DB,
   201  		p.ID,
   202  		*p.Strategy,
   203  		pageParams,
   204  	)
   205  	if err != nil {
   206  		return nil, shared.PageParams{}, err
   207  	}
   208  
   209  	pageParams.TotalRecords = totalRecords
   210  
   211  	return votes, pageParams, nil
   212  }
   213  
   214  func (h *Helpers) processVote(addr string, p models.Proposal) (*models.VoteWithBalance, error) {
   215  	vote, err := h.fetchVote(addr, p.ID)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	weight, err := h.useStrategyGetVoteWeight(p, vote)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	vote.Weight = &weight
   226  	return vote, err
   227  }
   228  
   229  func (h *Helpers) fetchVote(addr string, id int) (*models.VoteWithBalance, error) {
   230  	voteWithBalance := &models.VoteWithBalance{
   231  		Vote: models.Vote{
   232  			Addr:        addr,
   233  			Proposal_id: id,
   234  		}}
   235  
   236  	if err := voteWithBalance.GetVote(h.A.DB); err != nil {
   237  		switch err.Error() {
   238  		case pgx.ErrNoRows.Error():
   239  			msg := fmt.Sprintf("Vote not found.")
   240  			return nil, errors.New(msg)
   241  		default:
   242  			return nil, err
   243  		}
   244  	}
   245  
   246  	return voteWithBalance, nil
   247  }
   248  
   249  func (h *Helpers) processVotes(
   250  	addr string,
   251  	ids []int,
   252  	pageParams shared.PageParams,
   253  ) (
   254  	[]*models.VoteWithBalance,
   255  	shared.PageParams,
   256  	error,
   257  ) {
   258  	votes, totalRecords, err := models.GetVotesForAddress(
   259  		h.A.DB,
   260  		addr,
   261  		&ids,
   262  		pageParams,
   263  	)
   264  	if err != nil {
   265  		log.Error().Err(err).Msg("Error getting votes for address.")
   266  		return nil, pageParams, err
   267  	}
   268  
   269  	var votesWithBalances []*models.VoteWithBalance
   270  
   271  	for _, vote := range votes {
   272  		proposal := models.Proposal{ID: vote.Proposal_id}
   273  		if err := proposal.GetProposalById(h.A.DB); err != nil {
   274  			switch err.Error() {
   275  			case pgx.ErrNoRows.Error():
   276  				msg := fmt.Sprintf("Proposal with ID %d not found.", vote.Proposal_id)
   277  				return nil, pageParams, errors.New(msg)
   278  			default:
   279  				return nil, pageParams, err
   280  			}
   281  		}
   282  
   283  		s := h.initStrategy(*proposal.Strategy)
   284  		if s == nil {
   285  			return nil, pageParams, errors.New("Strategy not found.")
   286  		}
   287  
   288  		weight, err := s.GetVoteWeightForBalance(vote, &proposal)
   289  		if err != nil {
   290  			return nil, pageParams, err
   291  		}
   292  
   293  		vote.Weight = &weight
   294  		votesWithBalances = append(votesWithBalances, vote)
   295  	}
   296  
   297  	pageParams.TotalRecords = totalRecords
   298  
   299  	return votesWithBalances, pageParams, nil
   300  }
   301  
   302  func (h *Helpers) createVote(r *http.Request, p models.Proposal) (*models.VoteWithBalance, errorResponse) {
   303  	var v models.Vote
   304  	if err := validatePayload(r.Body, &v); err != nil {
   305  		log.Error().Err(err).Msg("Invalid request payload.")
   306  		return nil, errIncompleteRequest
   307  	}
   308  
   309  	v.Proposal_id = p.ID
   310  
   311  	// validate user hasn't already voted
   312  	existingVote := models.Vote{Proposal_id: v.Proposal_id, Addr: v.Addr}
   313  	if err := existingVote.GetVote(h.A.DB); err == nil {
   314  		errResponse := errAlreadyVoted
   315  		errResponse.Details = fmt.Sprintf(errResponse.Details, v.Addr, v.Proposal_id)
   316  		log.Error().Msgf(errResponse.Details)
   317  		return nil, errResponse
   318  	}
   319  
   320  	// check that proposal is live
   321  	if os.Getenv("APP_ENV") != "DEV" {
   322  		if !p.IsLive() {
   323  			return nil, errInactiveProposal
   324  		}
   325  	}
   326  
   327  	if errResponse := h.validateVote(p, v); errResponse != nilErr {
   328  		return nil, errResponse
   329  	}
   330  
   331  	v.Proposal_id = p.ID
   332  
   333  	s := h.initStrategy(*p.Strategy)
   334  	if s == nil {
   335  		return nil, errStrategyNotFound
   336  	}
   337  
   338  	voteWithBalance, errResponse := h.useStrategyFetchBalance(v, p, s)
   339  	if errResponse != nilErr {
   340  		return nil, errResponse
   341  	}
   342  
   343  	if errResponse := h.insertVote(voteWithBalance, p); errResponse != nilErr {
   344  		return nil, errResponse
   345  	}
   346  
   347  	return &voteWithBalance, nilErr
   348  }
   349  
   350  func (h *Helpers) insertVote(v models.VoteWithBalance, p models.Proposal) errorResponse {
   351  	weight, err := h.useStrategyGetVoteWeight(p, &v)
   352  	if err != nil {
   353  		log.Error().Err(err).Msgf("Error getting vote weight for address %s.", v.Addr)
   354  		return errIncompleteRequest
   355  	}
   356  
   357  	c := models.Community{ID: p.Community_id}
   358  	if err := c.GetCommunityByProposalId(h.A.DB, p.ID); err != nil {
   359  		return errGetCommunity
   360  	}
   361  
   362  	strategy, err := c.GetStrategy(*p.Strategy)
   363  	if err != nil {
   364  		return errStrategyNotFound
   365  	}
   366  
   367  	if err = p.ValidateBalance(weight); err != nil {
   368  		log.Error().Err(err).Msg("Account balance is too low to vote on this proposal.")
   369  		errResponse := errInsufficientBalance
   370  		errResponse.Details = fmt.Sprintf(errResponse.Details, *strategy.Threshold, *strategy.Contract.Name)
   371  		return errResponse
   372  	}
   373  
   374  	// Include voucher in vote data when pinning
   375  	ipfsVote := map[string]interface{}{
   376  		"vote": v,
   377  	}
   378  	v.Cid, err = h.pinJSONToIpfs(ipfsVote)
   379  	if err != nil {
   380  		log.Error().Err(err).Msg("Error pinning proposal to IPFS.")
   381  		return errCreateVote
   382  	}
   383  
   384  	if err := v.CreateVote(h.A.DB); err != nil {
   385  		msg := fmt.Sprintf("Error creating vote for address %s.", v.Addr)
   386  		log.Error().Err(err).Msg(msg)
   387  		return errCreateVote
   388  	}
   389  
   390  	return nilErr
   391  }
   392  
   393  func (h *Helpers) validateVote(p models.Proposal, v models.Vote) errorResponse {
   394  
   395  	// validate the user is not on community's blocklist
   396  	if err := h.validateBlocklist(v.Addr, p.Community_id); err != nil {
   397  		log.Error().Err(err).Msgf(fmt.Sprintf("Address %v is on blocklist for community id %v.\n", v.Addr, p.Community_id))
   398  		return errForbidden
   399  	}
   400  
   401  	// validate choice exists on proposal
   402  	if err := v.ValidateChoice(p); err != nil {
   403  		log.Error().Err(err)
   404  		return errIncompleteRequest
   405  	}
   406  
   407  	// If voucher is present
   408  	if v.Voucher != nil {
   409  		// Transaction Signature validation
   410  		voucher := v.Voucher
   411  		authorizer := voucher.Authorizers[0]
   412  
   413  		v.Composite_signatures = shared.GetUserCompositeSignatureFromVoucher(voucher)
   414  
   415  		// Validate authorizer
   416  		if authorizer != v.Addr || authorizer != (*v.Composite_signatures)[0].Addr {
   417  			err := errors.New("authorizer address must match voter address and envelope signer")
   418  			log.Error().Err(err)
   419  			return errIncompleteRequest
   420  		}
   421  
   422  		message := voucher.Arguments[0]["value"]
   423  
   424  		messageBytes, err := hex.DecodeString(message)
   425  		if err != nil {
   426  			log.Error().Err(err)
   427  			return errIncompleteRequest
   428  		}
   429  
   430  		// validate proper message format
   431  		//<proposalId>:<choice>:<timestamp>
   432  		if err := models.ValidateVoteMessage(string(messageBytes), p); err != nil {
   433  			log.Error().Err(err)
   434  			return errIncompleteRequest
   435  		}
   436  
   437  		// re-build message & composite signatures for validation
   438  		// set v.Message as the encoded message, rather than the colon(:) delimited message above.
   439  		// we can do this because we can always recover the tx arguments that make up the
   440  		// colon delimited message by decoding this rlp encoded message
   441  		v.Message = shared.EncodeMessageFromVoucher(voucher)
   442  
   443  		if err := h.validateTxSignature(v.Addr, v.Message, v.Composite_signatures); err != nil {
   444  			return errIncompleteRequest
   445  		}
   446  	} else {
   447  		// validate proper message format
   448  		// hex decode before validating
   449  		if err := models.ValidateVoteMessage(v.Message, p); err != nil {
   450  			log.Error().Err(err)
   451  			return errIncompleteRequest
   452  		}
   453  
   454  		if err := h.validateUserSignature(v.Addr, v.Message, v.Composite_signatures); err != nil {
   455  			return errIncompleteRequest
   456  		}
   457  	}
   458  
   459  	return nilErr
   460  }
   461  
   462  func (h *Helpers) fetchCommunity(id int) (models.Community, error) {
   463  	community := models.Community{ID: id}
   464  
   465  	if err := community.GetCommunity(h.A.DB); err != nil {
   466  		log.Error().Err(err)
   467  		return models.Community{}, err
   468  	}
   469  
   470  	return community, nil
   471  }
   472  
   473  func (h *Helpers) searchCommunities(
   474  	searchText string,
   475  	filters string,
   476  	pageParams shared.PageParams,
   477  ) (
   478  	[]*models.Community,
   479  	int,
   480  	map[string]int,
   481  	error,
   482  ) {
   483  	filtersSlice := strings.Split(filters, ",")
   484  	if searchText == "" {
   485  		isSearch := true
   486  
   487  		results, totalRecords, err := models.GetDefaultCommunities(
   488  			h.A.DB,
   489  			pageParams,
   490  			filtersSlice,
   491  			isSearch,
   492  		)
   493  		if err != nil {
   494  			log.Error().Err(err)
   495  			return nil, 0, nil, err
   496  		}
   497  
   498  		categoryCount, err := models.GetCategoryCount(h.A.DB, searchText)
   499  		if err != nil {
   500  			return []*models.Community{}, 0, nil, err
   501  		}
   502  
   503  		return results, totalRecords, categoryCount, nil
   504  	} else {
   505  		results, totalRecords, err := models.SearchForCommunity(
   506  			h.A.DB,
   507  			searchText,
   508  			filtersSlice,
   509  			pageParams,
   510  		)
   511  		if err != nil {
   512  			return []*models.Community{}, 0, nil, err
   513  		}
   514  
   515  		categoryCount, err := models.GetCategoryCount(h.A.DB, searchText)
   516  		if err != nil {
   517  			return []*models.Community{}, 0, nil, err
   518  		}
   519  
   520  		return results, totalRecords, categoryCount, nil
   521  	}
   522  }
   523  
   524  func (h *Helpers) createProposal(p models.Proposal) (models.Proposal, errorResponse) {
   525  	if err := h.validateStrategyName(*p.Strategy); err != nil {
   526  		fmt.Printf("Error validating strategy name: %v \n", err)
   527  		return models.Proposal{}, errStrategyNotFound
   528  	}
   529  
   530  	if p.Voucher != nil {
   531  		if err := h.validateUserViaVoucher(p.Creator_addr, p.Voucher); err != nil {
   532  			return models.Proposal{}, errForbidden
   533  		}
   534  	} else {
   535  		if err := h.validateUser(p.Creator_addr, p.Timestamp, p.Composite_signatures); err != nil {
   536  			return models.Proposal{}, errForbidden
   537  		}
   538  	}
   539  
   540  	community, err := h.fetchCommunity(p.Community_id)
   541  	if err != nil {
   542  		return models.Proposal{}, errIncompleteRequest
   543  	}
   544  
   545  	strategy, err := models.MatchStrategyByProposal(*community.Strategies, *p.Strategy)
   546  	if err != nil {
   547  		log.Error().Err(err).Msg("Community does not have this strategy available.")
   548  		return models.Proposal{}, errIncompleteRequest
   549  	}
   550  
   551  	// Set Min Balance/Max Weight to community defaults if not provided
   552  	if p.Min_balance == nil {
   553  		p.Min_balance = strategy.Contract.Threshold
   554  	}
   555  	if p.Max_weight == nil {
   556  		p.Max_weight = strategy.Contract.MaxWeight
   557  	}
   558  
   559  	header, err := h.A.FlowAdapter.LiveClient.GetLatestBlockHeader(context.Background(), true)
   560  	if err != nil {
   561  		log.Error().Err(err).Msg("Couldn't get block header")
   562  		return models.Proposal{}, errIncompleteRequest
   563  	}
   564  	p.Block_height = &header.Height
   565  
   566  	if err := h.enforceCommunityRestrictions(community, p, strategy); err != nil {
   567  		return models.Proposal{}, errIncompleteRequest
   568  	}
   569  
   570  	p.Cid, err = h.pinJSONToIpfs(p)
   571  	if err != nil {
   572  		log.Error().Err(err).Msg("IPFS error: " + err.Error())
   573  		return models.Proposal{}, errIncompleteRequest
   574  	}
   575  
   576  	validate := validator.New()
   577  	vErr := validate.Struct(p)
   578  	if vErr != nil {
   579  		log.Error().Err(vErr)
   580  		return models.Proposal{}, errIncompleteRequest
   581  	}
   582  
   583  	if os.Getenv("APP_ENV") == "PRODUCTION" {
   584  		if strategy.Contract.Name != nil && p.Start_time.Before(time.Now().UTC().Add(time.Hour)) {
   585  			p.Start_time = time.Now().UTC().Add(time.Hour)
   586  		}
   587  	}
   588  
   589  	if err := p.CreateProposal(h.A.DB); err != nil {
   590  		return models.Proposal{}, errIncompleteRequest
   591  	}
   592  
   593  	return p, nilErr
   594  }
   595  
   596  func (h *Helpers) validateStrategyName(name string) error {
   597  	if name == "" {
   598  		return errors.New("Strategy name is required.")
   599  	}
   600  
   601  	for k, _ := range strategyMap {
   602  		if name == k {
   603  			return nil
   604  		} else {
   605  			continue
   606  		}
   607  	}
   608  
   609  	return errors.New("Strategy not found.")
   610  }
   611  
   612  func (h *Helpers) enforceCommunityRestrictions(
   613  	c models.Community,
   614  	p models.Proposal,
   615  	s models.Strategy,
   616  ) error {
   617  
   618  	if *c.Only_authors_to_submit {
   619  		if err := models.EnsureRoleForCommunity(h.A.DB, p.Creator_addr, c.ID, "author"); err != nil {
   620  			errMsg := fmt.Sprintf("Account %s is not an author for community %d.", p.Creator_addr, p.Community_id)
   621  			log.Error().Err(err).Msg(errMsg)
   622  			return errors.New(errMsg)
   623  		}
   624  	} else {
   625  		fmt.Println("Community does not require authors to submit proposals")
   626  
   627  		threshold, err := strconv.ParseFloat(*c.Proposal_threshold, 64)
   628  		if err != nil {
   629  			log.Error().Err(err).Msg("Invalid proposal threshold")
   630  			return errors.New("Invalid proposal threshold")
   631  		}
   632  
   633  		contract := shared.Contract{
   634  			Name:        c.Contract_name,
   635  			Addr:        c.Contract_addr,
   636  			Public_path: c.Public_path,
   637  			Threshold:   &threshold,
   638  		}
   639  		hasBalance, err := h.processTokenThreshold(p.Creator_addr, contract, *c.Contract_type)
   640  		if err != nil {
   641  			errMsg := "Error processing Token Threshold."
   642  			log.Error().Err(err).Msg(errMsg)
   643  			return errors.New(errMsg)
   644  		}
   645  
   646  		if !hasBalance {
   647  			errMsg := "Insufficient token balance to create proposal."
   648  			log.Error().Err(err).Msg(errMsg)
   649  			return errors.New(errMsg)
   650  		}
   651  	}
   652  
   653  	return nil
   654  }
   655  
   656  func (h *Helpers) createCommunity(payload models.CreateCommunityRequestPayload) (models.Community, error) {
   657  	c := payload.Community
   658  
   659  	if c.Voucher != nil {
   660  		log.Info().Msgf("validate user via voucher %v \n", c.Voucher)
   661  		if err := h.validateUserViaVoucher(c.Creator_addr, c.Voucher); err != nil {
   662  			return models.Community{}, err
   663  		}
   664  	} else {
   665  		if err := h.validateUser(c.Creator_addr, c.Timestamp, c.Composite_signatures); err != nil {
   666  			return models.Community{}, err
   667  		}
   668  	}
   669  
   670  	cid, err := h.pinJSONToIpfs(c)
   671  	if err != nil {
   672  		log.Error().Err(err).Msg("Error pinning JSON to IPFS.")
   673  		return models.Community{}, err
   674  	}
   675  	c.Cid = cid
   676  
   677  	validate := validator.New()
   678  	vErr := validate.Struct(c)
   679  	if vErr != nil {
   680  		log.Error().Err(vErr).Msg("Invalid community.")
   681  		return models.Community{}, err
   682  	}
   683  
   684  	if err := c.CreateCommunity(h.A.DB); err != nil {
   685  		log.Error().Err(err).Msg("Database error creating community.")
   686  		return models.Community{}, err
   687  	}
   688  
   689  	if err := h.processCommunityRoles(&c, &payload); err != nil {
   690  		log.Error().Err(err).Msg("Error processing community roles.")
   691  		return models.Community{}, err
   692  	}
   693  
   694  	return c, nil
   695  }
   696  
   697  func (h *Helpers) processCommunityRoles(
   698  	c *models.Community,
   699  	p *models.CreateCommunityRequestPayload,
   700  ) error {
   701  	if err := models.GrantRolesToCommunityCreator(h.A.DB, c.Creator_addr, c.ID); err != nil {
   702  		errMsg := "Database error adding community creator roles."
   703  		log.Error().Err(err).Msg(errMsg)
   704  		return errors.New(errMsg)
   705  	}
   706  
   707  	if p.Additional_admins != nil {
   708  		for _, addr := range *p.Additional_admins {
   709  			if err := models.GrantAdminRolesToAddress(h.A.DB, c.ID, addr); err != nil {
   710  				log.Error().Err(err)
   711  				return err
   712  			}
   713  		}
   714  	}
   715  
   716  	if p.Additional_authors != nil {
   717  		for _, addr := range *p.Additional_authors {
   718  			if err := models.GrantAuthorRolesToAddress(h.A.DB, c.ID, addr); err != nil {
   719  				log.Error().Err(err)
   720  				return err
   721  			}
   722  		}
   723  	}
   724  
   725  	return nil
   726  }
   727  
   728  func (h *Helpers) updateCommunity(id int, payload models.UpdateCommunityRequestPayload) (models.Community, error) {
   729  	c, err := h.fetchCommunity(id)
   730  	if err != nil {
   731  		return models.Community{}, err
   732  	}
   733  
   734  	// validate is community creator
   735  	// TODO: update to validating address is admin
   736  	if err := c.CanUpdateCommunity(h.A.DB, payload.Signing_addr); err != nil {
   737  		log.Error().Err(err)
   738  		return models.Community{}, err
   739  	}
   740  
   741  	if payload.Voucher != nil {
   742  		if err := h.validateUserViaVoucher(payload.Signing_addr, payload.Voucher); err != nil {
   743  			log.Error().Err(err)
   744  			return models.Community{}, err
   745  		}
   746  	} else {
   747  		if err := h.validateUser(payload.Signing_addr, payload.Timestamp, payload.Composite_signatures); err != nil {
   748  			log.Error().Err(err)
   749  			return models.Community{}, err
   750  		}
   751  	}
   752  
   753  	if err := c.UpdateCommunity(h.A.DB, &payload); err != nil {
   754  		log.Error().Err(err)
   755  		return models.Community{}, err
   756  	}
   757  
   758  	c, err = h.fetchCommunity(id)
   759  	if err != nil {
   760  		return models.Community{}, err
   761  	}
   762  
   763  	return c, nil
   764  }
   765  
   766  func (h *Helpers) removeUserRole(payload models.CommunityUserPayload) (int, error) {
   767  	if payload.Voucher != nil {
   768  		if err := h.validateUserViaVoucher(payload.Signing_addr, payload.Voucher); err != nil {
   769  			log.Error().Err(err)
   770  			return http.StatusForbidden, err
   771  		}
   772  	} else {
   773  		if err := h.validateUser(payload.Signing_addr, payload.Timestamp, payload.Composite_signatures); err != nil {
   774  			log.Error().Err(err)
   775  			return http.StatusForbidden, err
   776  		}
   777  	}
   778  
   779  	if payload.User_type == "member" {
   780  		if payload.Addr == payload.Signing_addr {
   781  			// If a member is removing themselves, remove all their other roles as well
   782  			userRoles, err := models.GetAllRolesForUserInCommunity(h.A.DB, payload.Addr, payload.Community_id)
   783  			if err != nil {
   784  				log.Error().Err(err)
   785  				return http.StatusInternalServerError, err
   786  			}
   787  			for _, userRole := range userRoles {
   788  				if err := userRole.Remove(h.A.DB); err != nil {
   789  					log.Error().Err(err)
   790  					return http.StatusInternalServerError, err
   791  				}
   792  			}
   793  		} else {
   794  			// validate someone else is not removing a "member" role
   795  			CANNOT_REMOVE_MEMBER_ERR := errors.New("Cannot remove another member from a community.")
   796  			log.Error().Err(CANNOT_REMOVE_MEMBER_ERR)
   797  			return http.StatusForbidden, CANNOT_REMOVE_MEMBER_ERR
   798  		}
   799  	}
   800  
   801  	u := payload.CommunityUser
   802  
   803  	if payload.User_type == "admin" {
   804  		// validate signer is admin
   805  		var adminUser = models.CommunityUser{Addr: payload.Signing_addr, Community_id: payload.Community_id, User_type: "admin"}
   806  		if err := adminUser.GetCommunityUser(h.A.DB); err != nil {
   807  			USER_MUST_BE_ADMIN_ERR := errors.New("User must be community admin.")
   808  			log.Error().Err(err).Msg("Database error.")
   809  			log.Error().Err(USER_MUST_BE_ADMIN_ERR)
   810  			return http.StatusForbidden, USER_MUST_BE_ADMIN_ERR
   811  		}
   812  		// If the admin role is being removed, remove author role as well
   813  		author := models.CommunityUser{Addr: u.Addr, Community_id: u.Community_id, User_type: "author"}
   814  		if err := author.Remove(h.A.DB); err != nil {
   815  			return http.StatusInternalServerError, err
   816  		}
   817  		// remove admin role
   818  		if err := u.Remove(h.A.DB); err != nil {
   819  			return http.StatusInternalServerError, err
   820  		}
   821  		// Otherwise, just remove the specified user role
   822  	} else if err := u.Remove(h.A.DB); err != nil {
   823  		return http.StatusInternalServerError, err
   824  	}
   825  
   826  	return http.StatusOK, nil
   827  }
   828  
   829  func (h *Helpers) createCommunityUser(payload models.CommunityUserPayload) (int, error) {
   830  	// validate community_user payload fields
   831  	validate := validator.New()
   832  	vErr := validate.Struct(payload)
   833  	if vErr != nil {
   834  		errMsg := "Invalid community user."
   835  		log.Error().Err(vErr).Msg(errMsg)
   836  		return http.StatusBadRequest, errors.New(errMsg)
   837  	}
   838  	// validate user is allowed to create this user
   839  	if payload.User_type != "member" {
   840  		if payload.Signing_addr == payload.Addr {
   841  			CANNOT_GRANT_SELF_ERR := errors.New("Users cannot grant themselves a privileged user_type.")
   842  			log.Error().Err(CANNOT_GRANT_SELF_ERR)
   843  			return http.StatusForbidden, CANNOT_GRANT_SELF_ERR
   844  		}
   845  		// If signing address is not user address, verify they have admin status in this community
   846  		var communityAdmin = models.CommunityUser{Community_id: payload.Community_id, Addr: payload.Signing_addr, User_type: "admin"}
   847  		if err := communityAdmin.GetCommunityUser(h.A.DB); err != nil {
   848  			USER_MUST_BE_ADMIN_ERR := errors.New("User must be community admin to grant privileges.")
   849  			log.Error().Err(err).Msg("Database error.")
   850  			log.Error().Err(USER_MUST_BE_ADMIN_ERR)
   851  			return http.StatusForbidden, USER_MUST_BE_ADMIN_ERR
   852  		}
   853  	}
   854  	// only an account can add itself as a "member", unless an admin is granting
   855  	// an address a privileged role
   856  	if payload.User_type == "member" && payload.Addr != payload.Signing_addr {
   857  		CANNOT_ADD_MEMBER_ERR := errors.New(
   858  			"An account can only add itself as a community member, unless an admin is granting privileged role.",
   859  		)
   860  		log.Error().Err(CANNOT_ADD_MEMBER_ERR)
   861  		return http.StatusForbidden, CANNOT_ADD_MEMBER_ERR
   862  	}
   863  
   864  	if payload.Voucher != nil {
   865  		if err := h.validateUserViaVoucher(payload.Signing_addr, payload.Voucher); err != nil {
   866  			log.Error().Err(err)
   867  			return http.StatusForbidden, err
   868  		}
   869  	} else {
   870  		if err := h.validateUser(payload.Signing_addr, payload.Timestamp, payload.Composite_signatures); err != nil {
   871  			log.Error().Err(err)
   872  			return http.StatusForbidden, err
   873  		}
   874  	}
   875  
   876  	// check that community user doesnt already exist
   877  	// should throw a "ErrNoRows" error
   878  	u := payload.CommunityUser
   879  	if err := u.GetCommunityUser(h.A.DB); err == nil {
   880  		errMsg := fmt.Sprintf("Error: Address %s is already a %s of community %d.\n", u.Addr, u.User_type, u.Community_id)
   881  		log.Error().Err(err).Msg(errMsg)
   882  		return http.StatusBadRequest, errors.New(errMsg)
   883  	}
   884  
   885  	// Grant appropriate roles
   886  	if u.User_type == "admin" {
   887  		if err := models.GrantAdminRolesToAddress(h.A.DB, u.Community_id, u.Addr); err != nil {
   888  			log.Error().Err(err)
   889  			return http.StatusInternalServerError, err
   890  		}
   891  	} else if u.User_type == "author" {
   892  		if err := models.GrantAuthorRolesToAddress(h.A.DB, u.Community_id, u.Addr); err != nil {
   893  			return http.StatusInternalServerError, err
   894  		}
   895  	} else {
   896  		// grant member role
   897  		if err := u.CreateCommunityUser(h.A.DB); err != nil {
   898  			log.Error().Err(err)
   899  			return http.StatusInternalServerError, err
   900  		}
   901  	}
   902  
   903  	return http.StatusCreated, nil
   904  }
   905  
   906  func (h *Helpers) updateAddressesInList(id int, payload models.ListUpdatePayload, action string) (int, error) {
   907  	l := models.List{ID: id}
   908  
   909  	// get current proposal from DB
   910  	if err := l.GetListById(h.A.DB); err != nil {
   911  		errMsg := fmt.Sprintf("Error querying list with id %v.", id)
   912  		log.Error().Err(err).Msg(errMsg)
   913  		return http.StatusInternalServerError, err
   914  	}
   915  
   916  	validate := validator.New()
   917  	if vErr := validate.Struct(payload); vErr != nil {
   918  		errMsg := "Remove from list validation error."
   919  		if action == "add" {
   920  			errMsg = "Add to list validation error."
   921  		}
   922  		log.Error().Err(vErr).Msg(errMsg)
   923  		return http.StatusBadRequest, errors.New(errMsg)
   924  	}
   925  
   926  	if err := h.validateUserWithRole(payload.Signing_addr, payload.Timestamp, payload.Composite_signatures, l.Community_id, "admin"); err != nil {
   927  		log.Error().Err(err)
   928  		return http.StatusForbidden, err
   929  	}
   930  
   931  	if action == "remove" {
   932  		l.RemoveAddresses(payload.Addresses)
   933  	} else {
   934  		l.AddAddresses(payload.Addresses)
   935  	}
   936  
   937  	cid, err := h.pinJSONToIpfs(l)
   938  	if err != nil {
   939  		log.Error().Err(err).Msg("IPFS error: " + err.Error())
   940  		return http.StatusInternalServerError, errors.New("Error pinning JSON to IPFS.")
   941  	}
   942  	l.Cid = cid
   943  
   944  	if err := l.UpdateList(h.A.DB); err != nil {
   945  		errMsg := "Database error updating list."
   946  		log.Error().Err(err).Msg(errMsg)
   947  		return http.StatusInternalServerError, err
   948  	}
   949  
   950  	return http.StatusOK, nil
   951  }
   952  
   953  func (h *Helpers) createListForCommunity(payload models.ListPayload) (models.List, int, error) {
   954  	if existingList, _ := models.GetListForCommunityByType(h.A.DB, payload.Community_id, *payload.List_type); existingList.ID > 0 {
   955  		errMsg := fmt.Sprintf("List of type %s already exists for community %d.", *payload.List_type, payload.Community_id)
   956  		return models.List{}, http.StatusBadRequest, errors.New(errMsg)
   957  	}
   958  
   959  	// validate payload fields
   960  	validate := validator.New()
   961  	if vErr := validate.Struct(payload); vErr != nil {
   962  		errMsg := "Validation error in list payload."
   963  		log.Error().Err(vErr).Msg(errMsg)
   964  		return models.List{}, http.StatusBadRequest, errors.New(errMsg)
   965  	}
   966  
   967  	if err := h.validateUserWithRole(payload.Signing_addr, payload.Timestamp, payload.Composite_signatures, payload.Community_id, "admin"); err != nil {
   968  		log.Error().Err(err)
   969  		return models.List{}, http.StatusForbidden, err
   970  	}
   971  
   972  	l := payload.List
   973  
   974  	cid, err := h.pinJSONToIpfs(l)
   975  	if err != nil {
   976  		log.Error().Err(err).Msg("IPFS error: " + err.Error())
   977  		return models.List{}, http.StatusInternalServerError, errors.New("Error pinning JSON to IPFS.")
   978  	}
   979  	l.Cid = cid
   980  
   981  	// create list
   982  	if err := l.CreateList(h.A.DB); err != nil {
   983  		return models.List{}, http.StatusInternalServerError, err
   984  	}
   985  
   986  	return l, http.StatusCreated, nil
   987  }
   988  
   989  func (h *Helpers) validateUserSignature(addr string, message string, sigs *[]shared.CompositeSignature) error {
   990  	shouldValidateSignature := h.A.Config.Features["validateSigs"]
   991  
   992  	if !shouldValidateSignature {
   993  		return nil
   994  	}
   995  
   996  	hexMessage := hex.EncodeToString([]byte(message))
   997  	if err := h.A.FlowAdapter.ValidateSignature(addr, hexMessage, sigs, "USER"); err != nil {
   998  		return err
   999  	}
  1000  
  1001  	return nil
  1002  }
  1003  
  1004  func (h *Helpers) validateTxSignature(addr string, message string, sigs *[]shared.CompositeSignature) error {
  1005  	shouldValidateSignature := h.A.Config.Features["validateSigs"]
  1006  
  1007  	if !shouldValidateSignature {
  1008  		return nil
  1009  	}
  1010  
  1011  	if err := h.A.FlowAdapter.ValidateSignature(addr, message, sigs, "TRANSACTION"); err != nil {
  1012  		return err
  1013  	}
  1014  	return nil
  1015  }
  1016  
  1017  func (h *Helpers) validateBlocklist(addr string, communityId int) error {
  1018  	if !h.A.Config.Features["validateBlocklist"] {
  1019  		return nil
  1020  	}
  1021  
  1022  	blockList, _ := models.GetListForCommunityByType(h.A.DB, communityId, "block")
  1023  	isBlocked := funk.Contains(blockList.Addresses, addr)
  1024  
  1025  	isTest := flag.Lookup("test.v") != nil
  1026  
  1027  	if isBlocked && !isTest {
  1028  		return errors.New("User does not have permission.")
  1029  	}
  1030  	return nil
  1031  }
  1032  
  1033  // Need to move this to conditional middleware
  1034  func (h *Helpers) validateTimestamp(timestamp string, expiry int) error {
  1035  	if !h.A.Config.Features["validateTimestamps"] {
  1036  		return nil
  1037  	}
  1038  	// check timestamp and ensure no longer than expiry seconds has passed
  1039  	stamp, _ := strconv.ParseInt(timestamp, 10, 64)
  1040  	uxTime := time.Unix(stamp/1000, (stamp%1000)*1000*1000)
  1041  	diff := time.Now().UTC().Sub(uxTime).Seconds()
  1042  	if diff > float64(expiry) {
  1043  		err := errors.New("Timestamp on request has expired.")
  1044  		log.Error().Err(err).Msgf("expiry error: %v", diff)
  1045  		return err
  1046  	}
  1047  	return nil
  1048  }
  1049  
  1050  func (h *Helpers) validateUser(addr, timestamp string, compositeSignatures *[]shared.CompositeSignature) error {
  1051  
  1052  	if err := h.validateTimestamp(timestamp, 60); err != nil {
  1053  		return err
  1054  	}
  1055  
  1056  	if err := h.validateUserSignature(addr, timestamp, compositeSignatures); err != nil {
  1057  		return err
  1058  	}
  1059  
  1060  	return nil
  1061  }
  1062  
  1063  func (h *Helpers) validateUserViaVoucher(addr string, voucher *shared.Voucher) error {
  1064  	timestamp := voucher.Arguments[0]["value"]
  1065  	if err := h.validateTimestamp(timestamp, 60); err != nil {
  1066  		return err
  1067  	}
  1068  
  1069  	compositeSignatures := shared.GetUserCompositeSignatureFromVoucher(voucher)
  1070  	// Validate authorizer
  1071  	authorizer := voucher.Authorizers[0]
  1072  	if authorizer != addr || authorizer != (*compositeSignatures)[0].Addr {
  1073  		err := errors.New("authorizer address must match voter address and envelope signer")
  1074  		log.Error().Err(err)
  1075  		return err
  1076  	}
  1077  	// validate signature using encoded transaction payload as message
  1078  	message := shared.EncodeMessageFromVoucher(voucher)
  1079  	if err := h.validateTxSignature(addr, message, compositeSignatures); err != nil {
  1080  		return err
  1081  	}
  1082  
  1083  	return nil
  1084  }
  1085  
  1086  func (h *Helpers) validateUserWithRole(addr, timestamp string, compositeSignatures *[]shared.CompositeSignature, communityId int, role string) error {
  1087  	//print out all the params
  1088  	if err := h.validateTimestamp(timestamp, 60); err != nil {
  1089  		return err
  1090  	}
  1091  	if err := h.validateUserSignature(addr, timestamp, compositeSignatures); err != nil {
  1092  		return err
  1093  	}
  1094  	if err := models.EnsureRoleForCommunity(h.A.DB, addr, communityId, role); err != nil {
  1095  		errMsg := fmt.Sprintf("Account %s is not an author for community %d.", addr, communityId)
  1096  		log.Error().Err(err).Msg(errMsg)
  1097  		return err
  1098  	}
  1099  
  1100  	return nil
  1101  }
  1102  
  1103  func (h *Helpers) validateUserWithRoleViaVoucher(addr string, voucher *shared.Voucher, communityId int, role string) error {
  1104  	timestamp := voucher.Arguments[0]["value"]
  1105  	if err := h.validateTimestamp(timestamp, 60); err != nil {
  1106  		return err
  1107  	}
  1108  
  1109  	compositeSignatures := shared.GetUserCompositeSignatureFromVoucher(voucher)
  1110  	// Validate authorizer
  1111  	authorizer := voucher.Authorizers[0]
  1112  	if authorizer != addr || authorizer != (*compositeSignatures)[0].Addr {
  1113  		err := errors.New("authorizer address must match voter address and envelope signer")
  1114  		log.Error().Err(err)
  1115  		return err
  1116  	}
  1117  
  1118  	// validate signature using encoded transaction payload as message
  1119  	message := shared.EncodeMessageFromVoucher(voucher)
  1120  	if err := h.validateTxSignature(addr, message, compositeSignatures); err != nil {
  1121  		return err
  1122  	}
  1123  	if err := models.EnsureRoleForCommunity(h.A.DB, addr, communityId, role); err != nil {
  1124  		errMsg := fmt.Sprintf("Account %s is not an author for community %d.", addr, communityId)
  1125  		log.Error().Err(err).Msg(errMsg)
  1126  		return err
  1127  	}
  1128  
  1129  	return nil
  1130  }
  1131  
  1132  func (h *Helpers) processTokenThreshold(address string, c shared.Contract, contractType string) (bool, error) {
  1133  	var scriptPath string
  1134  
  1135  	if contractType == "nft" {
  1136  		scriptPath = "./main/cadence/scripts/get_nfts_ids.cdc"
  1137  	} else {
  1138  		scriptPath = "./main/cadence/scripts/get_balance.cdc"
  1139  	}
  1140  
  1141  	hasBalance, err := h.A.FlowAdapter.EnforceTokenThreshold(scriptPath, address, &c)
  1142  	if err != nil {
  1143  		return false, err
  1144  	}
  1145  
  1146  	return hasBalance, nil
  1147  }
  1148  
  1149  func (h *Helpers) initStrategy(name string) Strategy {
  1150  	s := strategyMap[name]
  1151  	if s == nil {
  1152  		return nil
  1153  	}
  1154  
  1155  	s.InitStrategy(h.A.FlowAdapter, h.A.DB)
  1156  
  1157  	return s
  1158  }
  1159  
  1160  func (h *Helpers) pinJSONToIpfs(data interface{}) (*string, error) {
  1161  	shouldOverride := flag.Lookup("ipfs-override").Value.(flag.Getter).Get().(bool)
  1162  	if shouldOverride {
  1163  		dummyHash := "dummy-hash"
  1164  		return &dummyHash, nil
  1165  	}
  1166  
  1167  	pin, err := h.A.IpfsClient.PinJson(data)
  1168  	if err != nil {
  1169  		return nil, err
  1170  	}
  1171  	return &pin.IpfsHash, nil
  1172  }
  1173  
  1174  func (h *Helpers) appendFiltersToResponse(
  1175  	results []*models.Community,
  1176  	pageParams shared.PageParams,
  1177  	count map[string]int,
  1178  ) (interface{}, error) {
  1179  	var filters []shared.SearchFilter
  1180  	var CATEGORIES = []string{
  1181  		"all",
  1182  		"dao",
  1183  		"social",
  1184  		"protocol",
  1185  		"creator",
  1186  		"nft",
  1187  		"collector",
  1188  	}
  1189  
  1190  	var totalCount int
  1191  	for _, category := range CATEGORIES {
  1192  		if category == "all" {
  1193  			continue
  1194  		}
  1195  		filters = append(filters, shared.SearchFilter{
  1196  			Text:   category,
  1197  			Amount: count[category],
  1198  		})
  1199  		totalCount += count[category]
  1200  	}
  1201  
  1202  	filters = append(filters, shared.SearchFilter{
  1203  		Text:   "all",
  1204  		Amount: totalCount,
  1205  	})
  1206  
  1207  	paginatedResults := shared.GetPaginatedResponseWithPayload(results, pageParams)
  1208  	appendedResponse := struct {
  1209  		Filters []shared.SearchFilter    `json:"filters"`
  1210  		Results shared.PaginatedResponse `json:"results"`
  1211  	}{
  1212  		Filters: filters,
  1213  		Results: *paginatedResults,
  1214  	}
  1215  
  1216  	return appendedResponse, nil
  1217  }
  1218  
  1219  func validateContractThreshold(s []models.Strategy) error {
  1220  	for _, s := range s {
  1221  		if s.Threshold != nil {
  1222  			if *s.Threshold < 1 {
  1223  				return errors.New("Contract Threshold cannot be less than 1.")
  1224  			}
  1225  		}
  1226  	}
  1227  	return nil
  1228  }
  1229  
  1230  func validateProposalThreshold(threshold string, onlyAuthorsToSubmit bool) error {
  1231  	propThreshold, err := strconv.ParseFloat(threshold, 64)
  1232  	if err != nil {
  1233  		return errors.New("Error Converting Proposal Threshold to Float.")
  1234  	}
  1235  	if !onlyAuthorsToSubmit && propThreshold < 1 {
  1236  		return errors.New("Proposal Threshold cannot be less than 1.")
  1237  	}
  1238  	return nil
  1239  }