github.com/status-im/status-go@v1.1.0/protocol/communities/permission_checker.go (about)

     1  package communities
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/big"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"go.uber.org/zap"
    12  
    13  	maps "golang.org/x/exp/maps"
    14  	slices "golang.org/x/exp/slices"
    15  
    16  	gethcommon "github.com/ethereum/go-ethereum/common"
    17  	"github.com/ethereum/go-ethereum/common/hexutil"
    18  	"github.com/status-im/status-go/protocol/ens"
    19  	"github.com/status-im/status-go/protocol/protobuf"
    20  	walletcommon "github.com/status-im/status-go/services/wallet/common"
    21  	"github.com/status-im/status-go/services/wallet/thirdparty"
    22  )
    23  
    24  type PermissionChecker interface {
    25  	CheckPermissionToJoin(*Community, []gethcommon.Address) (*CheckPermissionToJoinResponse, error)
    26  	CheckPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error)
    27  	CheckPermissionsWithPreFetchedData(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool, collectiblesOwners CollectiblesOwners) (*CheckPermissionsResponse, error)
    28  }
    29  
    30  type DefaultPermissionChecker struct {
    31  	tokenManager        TokenManager
    32  	collectiblesManager CollectiblesManager
    33  	ensVerifier         *ens.Verifier
    34  
    35  	logger *zap.Logger
    36  }
    37  
    38  type PreParsedPermissionsData struct {
    39  	Erc721TokenRequirements map[uint64]map[string]*protobuf.TokenCriteria
    40  	Erc20TokenAddresses     []gethcommon.Address
    41  	Erc20ChainIDsMap        map[uint64]bool
    42  	Erc721ChainIDsMap       map[uint64]bool
    43  }
    44  
    45  type PreParsedCommunityPermissionsData struct {
    46  	*PreParsedPermissionsData
    47  	Permissions []*CommunityTokenPermission
    48  }
    49  
    50  func (p *DefaultPermissionChecker) getOwnedENS(addresses []gethcommon.Address) ([]string, error) {
    51  	ownedENS := make([]string, 0)
    52  	if p.ensVerifier == nil {
    53  		p.logger.Warn("no ensVerifier configured for communities manager")
    54  		return ownedENS, nil
    55  	}
    56  	for _, address := range addresses {
    57  		name, err := p.ensVerifier.ReverseResolve(address)
    58  		if err != nil && err.Error() != "not a resolver" {
    59  			return ownedENS, err
    60  		}
    61  		if name != "" {
    62  			ownedENS = append(ownedENS, name)
    63  		}
    64  	}
    65  	return ownedENS, nil
    66  }
    67  
    68  type collectiblesBalancesGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error)
    69  
    70  func (p *DefaultPermissionChecker) getOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64, getCollectiblesBalances collectiblesBalancesGetter) (CollectiblesByChain, error) {
    71  	if p.collectiblesManager == nil {
    72  		return nil, errors.New("no collectibles manager")
    73  	}
    74  
    75  	ctx := context.Background()
    76  
    77  	ownedERC721Tokens := make(CollectiblesByChain)
    78  
    79  	for chainID, erc721Tokens := range tokenRequirements {
    80  
    81  		skipChain := true
    82  		for _, cID := range chainIDs {
    83  			if chainID == cID {
    84  				skipChain = false
    85  			}
    86  		}
    87  
    88  		if skipChain {
    89  			continue
    90  		}
    91  
    92  		contractAddresses := make([]gethcommon.Address, 0)
    93  		for contractAddress := range erc721Tokens {
    94  			contractAddresses = append(contractAddresses, gethcommon.HexToAddress(contractAddress))
    95  		}
    96  
    97  		if _, exists := ownedERC721Tokens[chainID]; !exists {
    98  			ownedERC721Tokens[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
    99  		}
   100  
   101  		for _, owner := range walletAddresses {
   102  			balances, err := getCollectiblesBalances(ctx, walletcommon.ChainID(chainID), owner, contractAddresses)
   103  			if err != nil {
   104  				p.logger.Info("couldn't fetch owner assets", zap.Error(err))
   105  				return nil, err
   106  			}
   107  			ownedERC721Tokens[chainID][owner] = balances
   108  		}
   109  	}
   110  	return ownedERC721Tokens, nil
   111  }
   112  
   113  func (p *DefaultPermissionChecker) accountChainsCombinationToMap(combinations []*AccountChainIDsCombination) map[gethcommon.Address][]uint64 {
   114  	result := make(map[gethcommon.Address][]uint64)
   115  	for _, combination := range combinations {
   116  		result[combination.Address] = combination.ChainIDs
   117  	}
   118  	return result
   119  }
   120  
   121  // merge valid combinations w/o duplicates
   122  func (p *DefaultPermissionChecker) MergeValidCombinations(left, right []*AccountChainIDsCombination) []*AccountChainIDsCombination {
   123  
   124  	leftMap := p.accountChainsCombinationToMap(left)
   125  	rightMap := p.accountChainsCombinationToMap(right)
   126  
   127  	// merge maps, result in left map
   128  	for k, v := range rightMap {
   129  		if _, exists := leftMap[k]; !exists {
   130  			leftMap[k] = v
   131  			continue
   132  		} else {
   133  			// append chains which are new
   134  			chains := leftMap[k]
   135  			for _, chainID := range v {
   136  				if !slices.Contains(chains, chainID) {
   137  					chains = append(chains, chainID)
   138  				}
   139  			}
   140  			leftMap[k] = chains
   141  		}
   142  	}
   143  
   144  	result := []*AccountChainIDsCombination{}
   145  	for k, v := range leftMap {
   146  		result = append(result, &AccountChainIDsCombination{
   147  			Address:  k,
   148  			ChainIDs: v,
   149  		})
   150  	}
   151  
   152  	return result
   153  }
   154  
   155  func (p *DefaultPermissionChecker) CheckPermissionToJoin(community *Community, addresses []gethcommon.Address) (*CheckPermissionToJoinResponse, error) {
   156  	becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN)
   157  	becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)
   158  	becomeTokenMasterPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)
   159  
   160  	adminOrTokenMasterPermissionsToJoin := append(becomeAdminPermissions, becomeTokenMasterPermissions...)
   161  
   162  	allChainIDs, err := p.tokenManager.GetAllChainIDs()
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs)
   168  
   169  	// Check becomeMember and (admin & token master) permissions separately.
   170  	becomeMemberPermissionsResponse, err := p.checkPermissionsOrDefault(becomeMemberPermissions, accountsAndChainIDs)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	if len(adminOrTokenMasterPermissionsToJoin) <= 0 {
   176  		return becomeMemberPermissionsResponse, nil
   177  	}
   178  	// If there are any admin or token master permissions, combine result.
   179  	preParsedPermissions := preParsedCommunityPermissionsData(adminOrTokenMasterPermissionsToJoin)
   180  	var adminOrTokenPermissionsResponse *CheckPermissionsResponse
   181  
   182  	if community.IsControlNode() {
   183  		adminOrTokenPermissionsResponse, err = p.CheckPermissions(preParsedPermissions, accountsAndChainIDs, false)
   184  	} else {
   185  		adminOrTokenPermissionsResponse, err = p.CheckCachedPermissions(preParsedPermissions, accountsAndChainIDs, false)
   186  	}
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	mergedPermissions := make(map[string]*PermissionTokenCriteriaResult)
   192  	maps.Copy(mergedPermissions, becomeMemberPermissionsResponse.Permissions)
   193  	maps.Copy(mergedPermissions, adminOrTokenPermissionsResponse.Permissions)
   194  
   195  	mergedCombinations := p.MergeValidCombinations(becomeMemberPermissionsResponse.ValidCombinations, adminOrTokenPermissionsResponse.ValidCombinations)
   196  
   197  	combinedResponse := &CheckPermissionsResponse{
   198  		Satisfied:         becomeMemberPermissionsResponse.Satisfied || adminOrTokenPermissionsResponse.Satisfied,
   199  		Permissions:       mergedPermissions,
   200  		ValidCombinations: mergedCombinations,
   201  	}
   202  
   203  	return combinedResponse, nil
   204  }
   205  
   206  func (p *DefaultPermissionChecker) checkPermissionsOrDefault(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination) (*CheckPermissionsResponse, error) {
   207  	if len(permissions) == 0 {
   208  		// There are no permissions to join on this community at the moment,
   209  		// so we reveal all accounts + all chain IDs
   210  		response := &CheckPermissionsResponse{
   211  			Satisfied:         true,
   212  			Permissions:       make(map[string]*PermissionTokenCriteriaResult),
   213  			ValidCombinations: accountsAndChainIDs,
   214  		}
   215  		return response, nil
   216  	}
   217  
   218  	preParsedPermissions := preParsedCommunityPermissionsData(permissions)
   219  	return p.CheckCachedPermissions(preParsedPermissions, accountsAndChainIDs, false)
   220  }
   221  
   222  type ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error)
   223  type balancesByChainGetter = func(ctx context.Context, accounts, tokens []gethcommon.Address, chainIDs []uint64) (BalancesByChain, error)
   224  
   225  func (p *DefaultPermissionChecker) checkTokenRequirement(
   226  	tokenRequirement *protobuf.TokenCriteria,
   227  	accounts []gethcommon.Address, ownedERC20TokenBalances BalancesByChain, ownedERC721Tokens CollectiblesByChain,
   228  	accountsChainIDsCombinations map[gethcommon.Address]map[uint64]bool,
   229  ) (TokenRequirementResponse, error) {
   230  	tokenRequirementResponse := TokenRequirementResponse{TokenCriteria: tokenRequirement}
   231  
   232  	switch tokenRequirement.Type {
   233  
   234  	case protobuf.CommunityTokenType_ERC721:
   235  
   236  		if len(ownedERC721Tokens) == 0 {
   237  			return tokenRequirementResponse, nil
   238  		}
   239  
   240  		// Limit NFTs count to uint32
   241  		requiredCount, err := strconv.ParseUint(tokenRequirement.AmountInWei, 10, 32)
   242  		if err != nil {
   243  			return tokenRequirementResponse, fmt.Errorf("invalid ERC721 amount: %s", tokenRequirement.AmountInWei)
   244  		}
   245  		accumulatedCount := uint64(0)
   246  
   247  		for chainID, addressStr := range tokenRequirement.ContractAddresses {
   248  			contractAddress := gethcommon.HexToAddress(addressStr)
   249  			if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 {
   250  				continue
   251  			}
   252  
   253  			for account := range ownedERC721Tokens[chainID] {
   254  				if _, exists := ownedERC721Tokens[chainID][account]; !exists {
   255  					continue
   256  				}
   257  
   258  				tokenBalances := ownedERC721Tokens[chainID][account][contractAddress]
   259  				accumulatedCount += uint64(len(tokenBalances))
   260  
   261  				if len(tokenBalances) > 0 {
   262  					// 'account' owns some TokenID owned from contract 'address'
   263  					if _, exists := accountsChainIDsCombinations[account]; !exists {
   264  						accountsChainIDsCombinations[account] = make(map[uint64]bool)
   265  					}
   266  
   267  					// account has balance > 0 on this chain for this token, so let's add it the chain IDs
   268  					accountsChainIDsCombinations[account][chainID] = true
   269  
   270  					if len(tokenRequirement.TokenIds) == 0 {
   271  						// no specific tokenId of this collection is needed
   272  
   273  						if accumulatedCount >= requiredCount {
   274  							tokenRequirementResponse.Satisfied = true
   275  							return tokenRequirementResponse, nil
   276  						}
   277  					}
   278  
   279  					for _, tokenID := range tokenRequirement.TokenIds {
   280  						tokenIDBigInt := new(big.Int).SetUint64(tokenID)
   281  
   282  						for _, asset := range tokenBalances {
   283  							if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
   284  								tokenRequirementResponse.Satisfied = true
   285  								return tokenRequirementResponse, nil
   286  							}
   287  						}
   288  					}
   289  				}
   290  			}
   291  		}
   292  
   293  	case protobuf.CommunityTokenType_ERC20:
   294  
   295  		if len(ownedERC20TokenBalances) == 0 {
   296  			return tokenRequirementResponse, nil
   297  		}
   298  
   299  		accumulatedBalance := new(big.Int)
   300  
   301  	chainIDLoopERC20:
   302  		for chainID, address := range tokenRequirement.ContractAddresses {
   303  			if _, exists := ownedERC20TokenBalances[chainID]; !exists || len(ownedERC20TokenBalances[chainID]) == 0 {
   304  				continue chainIDLoopERC20
   305  			}
   306  			contractAddress := gethcommon.HexToAddress(address)
   307  			for account := range ownedERC20TokenBalances[chainID] {
   308  				if _, exists := ownedERC20TokenBalances[chainID][account][contractAddress]; !exists {
   309  					continue
   310  				}
   311  
   312  				value := ownedERC20TokenBalances[chainID][account][contractAddress]
   313  
   314  				if _, exists := accountsChainIDsCombinations[account]; !exists {
   315  					accountsChainIDsCombinations[account] = make(map[uint64]bool)
   316  				}
   317  
   318  				if value.ToInt().Cmp(big.NewInt(0)) > 0 {
   319  					// account has balance > 0 on this chain for this token, so let's add it the chain IDs
   320  					accountsChainIDsCombinations[account][chainID] = true
   321  				}
   322  
   323  				// check if adding current chain account balance to accumulated balance
   324  				// satisfies required amount
   325  				prevBalance := accumulatedBalance
   326  				accumulatedBalance.Add(prevBalance, value.ToInt())
   327  
   328  				requiredAmount, success := new(big.Int).SetString(tokenRequirement.AmountInWei, 10)
   329  				if !success {
   330  					return tokenRequirementResponse, fmt.Errorf("amountInWeis value is incorrect: %s", tokenRequirement.AmountInWei)
   331  				}
   332  
   333  				if accumulatedBalance.Cmp(requiredAmount) != -1 {
   334  					tokenRequirementResponse.Satisfied = true
   335  					return tokenRequirementResponse, nil
   336  				}
   337  			}
   338  		}
   339  
   340  	case protobuf.CommunityTokenType_ENS:
   341  
   342  		for _, account := range accounts {
   343  			ownedENSNames, err := p.getOwnedENS([]gethcommon.Address{account})
   344  			if err != nil {
   345  				return tokenRequirementResponse, err
   346  			}
   347  
   348  			if _, exists := accountsChainIDsCombinations[account]; !exists {
   349  				accountsChainIDsCombinations[account] = make(map[uint64]bool)
   350  			}
   351  
   352  			if !strings.HasPrefix(tokenRequirement.EnsPattern, "*.") {
   353  				for _, ownedENS := range ownedENSNames {
   354  					if ownedENS == tokenRequirement.EnsPattern {
   355  						accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
   356  						tokenRequirementResponse.Satisfied = true
   357  						return tokenRequirementResponse, nil
   358  					}
   359  				}
   360  			} else {
   361  				parentName := tokenRequirement.EnsPattern[2:]
   362  				for _, ownedENS := range ownedENSNames {
   363  					if strings.HasSuffix(ownedENS, parentName) {
   364  						accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
   365  						tokenRequirementResponse.Satisfied = true
   366  						return tokenRequirementResponse, nil
   367  					}
   368  				}
   369  			}
   370  		}
   371  
   372  	}
   373  
   374  	return tokenRequirementResponse, nil
   375  }
   376  
   377  func (p *DefaultPermissionChecker) checkPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool,
   378  	getOwnedERC721Tokens ownedERC721TokensGetter, getBalancesByChain balancesByChainGetter) (*CheckPermissionsResponse, error) {
   379  
   380  	response := &CheckPermissionsResponse{
   381  		Satisfied:         false,
   382  		Permissions:       make(map[string]*PermissionTokenCriteriaResult),
   383  		ValidCombinations: make([]*AccountChainIDsCombination, 0),
   384  	}
   385  
   386  	if permissionsParsedData == nil {
   387  		response.Satisfied = true
   388  		return response, nil
   389  	}
   390  
   391  	erc721TokenRequirements := permissionsParsedData.Erc721TokenRequirements
   392  
   393  	erc20ChainIDsMap := permissionsParsedData.Erc20ChainIDsMap
   394  	erc721ChainIDsMap := permissionsParsedData.Erc721ChainIDsMap
   395  
   396  	erc20TokenAddresses := permissionsParsedData.Erc20TokenAddresses
   397  
   398  	accounts := make([]gethcommon.Address, 0)
   399  
   400  	// TODO: move outside in order not to convert it
   401  	for _, accountAndChainIDs := range accountsAndChainIDs {
   402  		accounts = append(accounts, accountAndChainIDs.Address)
   403  	}
   404  
   405  	chainIDsForERC20 := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsMap)
   406  	chainIDsForERC721 := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsMap)
   407  
   408  	// if there are no chain IDs that match token criteria chain IDs
   409  	// we aren't able to check balances on selected networks
   410  	if len(erc20ChainIDsMap) > 0 && len(chainIDsForERC20) == 0 {
   411  		response.NetworksNotSupported = true
   412  		return response, nil
   413  	}
   414  
   415  	ownedERC20TokenBalances := make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, 0)
   416  	if len(chainIDsForERC20) > 0 {
   417  		// this only returns balances for the networks we're actually interested in
   418  		balances, err := getBalancesByChain(context.Background(), accounts, erc20TokenAddresses, chainIDsForERC20)
   419  		if err != nil {
   420  			return nil, err
   421  		}
   422  		ownedERC20TokenBalances = balances
   423  	}
   424  
   425  	ownedERC721Tokens := make(CollectiblesByChain)
   426  	if len(chainIDsForERC721) > 0 {
   427  		collectibles, err := getOwnedERC721Tokens(accounts, erc721TokenRequirements, chainIDsForERC721)
   428  		if err != nil {
   429  			return nil, err
   430  		}
   431  		ownedERC721Tokens = collectibles
   432  	}
   433  
   434  	accountsChainIDsCombinations := make(map[gethcommon.Address]map[uint64]bool)
   435  
   436  	for _, tokenPermission := range permissionsParsedData.Permissions {
   437  		permissionRequirementsMet := true
   438  		response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{Role: tokenPermission.Type}
   439  
   440  		// There can be multiple token requirements per permission.
   441  		// If only one is not met, the entire permission is marked
   442  		// as not fulfilled
   443  		for _, tokenRequirement := range tokenPermission.TokenCriteria {
   444  			tokenRequirementResponse, err := p.checkTokenRequirement(tokenRequirement, accounts, ownedERC20TokenBalances, ownedERC721Tokens, accountsChainIDsCombinations)
   445  			if err != nil {
   446  				p.logger.Error("failed to check token requirement", zap.Error(err))
   447  			}
   448  
   449  			if !tokenRequirementResponse.Satisfied {
   450  				permissionRequirementsMet = false
   451  			}
   452  
   453  			response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
   454  			response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, tokenRequirementResponse.Satisfied)
   455  		}
   456  		response.Permissions[tokenPermission.Id].ID = tokenPermission.Id
   457  
   458  		// multiple permissions are treated as logical OR, meaning
   459  		// if only one of them is fulfilled, the user gets permission
   460  		// to join and we can stop early
   461  		if shortcircuit && permissionRequirementsMet {
   462  			break
   463  		}
   464  	}
   465  
   466  	// attach valid account and chainID combinations to response
   467  	for account, chainIDs := range accountsChainIDsCombinations {
   468  		combination := &AccountChainIDsCombination{
   469  			Address: account,
   470  		}
   471  		for chainID := range chainIDs {
   472  			combination.ChainIDs = append(combination.ChainIDs, chainID)
   473  		}
   474  		response.ValidCombinations = append(response.ValidCombinations, combination)
   475  	}
   476  
   477  	response.calculateSatisfied()
   478  
   479  	return response, nil
   480  }
   481  
   482  type balancesByOwnerAndContractAddressGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (map[gethcommon.Address][]thirdparty.TokenBalance, error)
   483  
   484  func (p *DefaultPermissionChecker) handlePermissionsCheck(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool,
   485  	getBalancesByOwnerAndContractAddress balancesByOwnerAndContractAddressGetter,
   486  	getBalancesByChain balancesByChainGetter) (*CheckPermissionsResponse, error) {
   487  
   488  	var getOwnedERC721Tokens ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
   489  		return p.getOwnedERC721Tokens(walletAddresses, tokenRequirements, chainIDs, getBalancesByOwnerAndContractAddress)
   490  	}
   491  
   492  	return p.checkPermissions(permissionsParsedData, accountsAndChainIDs, shortcircuit, getOwnedERC721Tokens, getBalancesByChain)
   493  }
   494  
   495  func (p *DefaultPermissionChecker) CheckCachedPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
   496  	return p.handlePermissionsCheck(permissionsParsedData, accountsAndChainIDs, shortcircuit, p.collectiblesManager.FetchCachedBalancesByOwnerAndContractAddress, p.tokenManager.GetCachedBalancesByChain)
   497  }
   498  
   499  // CheckPermissions will retrieve balances and check whether the user has
   500  // permission to join the community, if shortcircuit is true, it will stop as soon
   501  // as we know the answer
   502  func (p *DefaultPermissionChecker) CheckPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
   503  	return p.handlePermissionsCheck(permissionsParsedData, accountsAndChainIDs, shortcircuit, p.collectiblesManager.FetchBalancesByOwnerAndContractAddress, p.tokenManager.GetBalancesByChain)
   504  }
   505  
   506  type CollectiblesOwners = map[walletcommon.ChainID]map[gethcommon.Address]*thirdparty.CollectibleContractOwnership
   507  
   508  // Same as CheckPermissions but relies on already provided collectibles owners
   509  func (p *DefaultPermissionChecker) CheckPermissionsWithPreFetchedData(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool, collectiblesOwners CollectiblesOwners) (*CheckPermissionsResponse, error) {
   510  	var getCollectiblesBalances collectiblesBalancesGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
   511  		ret := make(thirdparty.TokenBalancesPerContractAddress)
   512  
   513  		collectiblesByChain, ok := collectiblesOwners[chainID]
   514  		if !ok {
   515  			return nil, errors.New("no data available for chainID")
   516  		}
   517  
   518  		for _, contractAddress := range contractAddresses {
   519  			ownership, ok := collectiblesByChain[contractAddress]
   520  			if !ok {
   521  				return nil, errors.New("no data available for collectible")
   522  			}
   523  
   524  			for _, nftOwner := range ownership.Owners {
   525  				if nftOwner.OwnerAddress == ownerAddress {
   526  					ret[contractAddress] = nftOwner.TokenBalances
   527  					break
   528  				}
   529  			}
   530  		}
   531  
   532  		return ret, nil
   533  	}
   534  
   535  	var getOwnedERC721Tokens ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
   536  		return p.getOwnedERC721Tokens(walletAddresses, tokenRequirements, chainIDs, getCollectiblesBalances)
   537  	}
   538  
   539  	return p.checkPermissions(permissionsParsedData, accountsAndChainIDs, shortcircuit, getOwnedERC721Tokens, p.tokenManager.GetBalancesByChain)
   540  }
   541  
   542  func preParsedPermissionsData(permissions []*CommunityTokenPermission) *PreParsedPermissionsData {
   543  	erc20TokenRequirements, erc721TokenRequirements, _ := ExtractTokenCriteria(permissions)
   544  
   545  	erc20ChainIDsMap := make(map[uint64]bool)
   546  	erc721ChainIDsMap := make(map[uint64]bool)
   547  
   548  	erc20TokenAddresses := make([]gethcommon.Address, 0)
   549  
   550  	// figure out chain IDs we're interested in
   551  	for chainID, tokens := range erc20TokenRequirements {
   552  		erc20ChainIDsMap[chainID] = true
   553  		for contractAddress := range tokens {
   554  			erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress))
   555  		}
   556  	}
   557  
   558  	for chainID := range erc721TokenRequirements {
   559  		erc721ChainIDsMap[chainID] = true
   560  	}
   561  
   562  	return &PreParsedPermissionsData{
   563  		Erc721TokenRequirements: erc721TokenRequirements,
   564  		Erc20TokenAddresses:     erc20TokenAddresses,
   565  		Erc20ChainIDsMap:        erc20ChainIDsMap,
   566  		Erc721ChainIDsMap:       erc721ChainIDsMap,
   567  	}
   568  }
   569  
   570  func preParsedCommunityPermissionsData(permissions []*CommunityTokenPermission) *PreParsedCommunityPermissionsData {
   571  	if len(permissions) == 0 {
   572  		return nil
   573  	}
   574  
   575  	return &PreParsedCommunityPermissionsData{
   576  		Permissions:              permissions,
   577  		PreParsedPermissionsData: preParsedPermissionsData(permissions),
   578  	}
   579  }
   580  
   581  func PreParsePermissionsData(permissions map[string]*CommunityTokenPermission) (map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData, map[string]*PreParsedCommunityPermissionsData) {
   582  	becomeMemberPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_MEMBER)
   583  	becomeAdminPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_ADMIN)
   584  	becomeTokenMasterPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)
   585  
   586  	viewOnlyPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
   587  	viewAndPostPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
   588  	channelPermissions := append(viewAndPostPermissions, viewOnlyPermissions...)
   589  
   590  	communityPermissionsPreParsedData := make(map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData)
   591  	communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_MEMBER] = preParsedCommunityPermissionsData(becomeMemberPermissions)
   592  	communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_ADMIN] = preParsedCommunityPermissionsData(becomeAdminPermissions)
   593  	communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER] = preParsedCommunityPermissionsData(becomeTokenMasterPermissions)
   594  
   595  	channelPermissionsPreParsedData := make(map[string]*PreParsedCommunityPermissionsData)
   596  	for _, channelPermission := range channelPermissions {
   597  		channelPermissionsPreParsedData[channelPermission.Id] = preParsedCommunityPermissionsData([]*CommunityTokenPermission{channelPermission})
   598  	}
   599  
   600  	return communityPermissionsPreParsedData, channelPermissionsPreParsedData
   601  }
   602  
   603  func CollectibleAddressesFromPreParsedPermissionsData(communityPermissions map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData, channelPermissions map[string]*PreParsedCommunityPermissionsData) map[walletcommon.ChainID]map[gethcommon.Address]struct{} {
   604  	ret := make(map[walletcommon.ChainID]map[gethcommon.Address]struct{})
   605  
   606  	allPermissionsData := []*PreParsedCommunityPermissionsData{}
   607  	for _, permissionsData := range communityPermissions {
   608  		if permissionsData != nil {
   609  			allPermissionsData = append(allPermissionsData, permissionsData)
   610  		}
   611  	}
   612  	for _, permissionsData := range channelPermissions {
   613  		if permissionsData != nil {
   614  			allPermissionsData = append(allPermissionsData, permissionsData)
   615  		}
   616  	}
   617  
   618  	for _, data := range allPermissionsData {
   619  		for chainID, contractAddresses := range data.Erc721TokenRequirements {
   620  			if ret[walletcommon.ChainID(chainID)] == nil {
   621  				ret[walletcommon.ChainID(chainID)] = make(map[gethcommon.Address]struct{})
   622  			}
   623  
   624  			for contractAddress := range contractAddresses {
   625  				ret[walletcommon.ChainID(chainID)][gethcommon.HexToAddress(contractAddress)] = struct{}{}
   626  			}
   627  		}
   628  	}
   629  
   630  	return ret
   631  }