status-im/status-go

View on GitHub
protocol/communities/permission_checker.go

Summary

Maintainability
A
0 mins
Test Coverage
B
82%
package communities

import (
    "context"
    "errors"
    "fmt"
    "math/big"
    "strconv"
    "strings"

    "go.uber.org/zap"

    maps "golang.org/x/exp/maps"
    slices "golang.org/x/exp/slices"

    gethcommon "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/status-im/status-go/protocol/ens"
    "github.com/status-im/status-go/protocol/protobuf"
    walletcommon "github.com/status-im/status-go/services/wallet/common"
    "github.com/status-im/status-go/services/wallet/thirdparty"
)

type PermissionChecker interface {
    CheckPermissionToJoin(*Community, []gethcommon.Address) (*CheckPermissionToJoinResponse, error)
    CheckPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error)
    CheckPermissionsWithPreFetchedData(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool, collectiblesOwners CollectiblesOwners) (*CheckPermissionsResponse, error)
}

type DefaultPermissionChecker struct {
    tokenManager        TokenManager
    collectiblesManager CollectiblesManager
    ensVerifier         *ens.Verifier

    logger *zap.Logger
}

type PreParsedPermissionsData struct {
    Erc721TokenRequirements map[uint64]map[string]*protobuf.TokenCriteria
    Erc20TokenAddresses     []gethcommon.Address
    Erc20ChainIDsMap        map[uint64]bool
    Erc721ChainIDsMap       map[uint64]bool
}

type PreParsedCommunityPermissionsData struct {
    *PreParsedPermissionsData
    Permissions []*CommunityTokenPermission
}

func (p *DefaultPermissionChecker) getOwnedENS(addresses []gethcommon.Address) ([]string, error) {
    ownedENS := make([]string, 0)
    if p.ensVerifier == nil {
        p.logger.Warn("no ensVerifier configured for communities manager")
        return ownedENS, nil
    }
    for _, address := range addresses {
        name, err := p.ensVerifier.ReverseResolve(address)
        if err != nil && err.Error() != "not a resolver" {
            return ownedENS, err
        }
        if name != "" {
            ownedENS = append(ownedENS, name)
        }
    }
    return ownedENS, nil
}

type collectiblesBalancesGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error)

func (p *DefaultPermissionChecker) getOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64, getCollectiblesBalances collectiblesBalancesGetter) (CollectiblesByChain, error) {
    if p.collectiblesManager == nil {
        return nil, errors.New("no collectibles manager")
    }

    ctx := context.Background()

    ownedERC721Tokens := make(CollectiblesByChain)

    for chainID, erc721Tokens := range tokenRequirements {

        skipChain := true
        for _, cID := range chainIDs {
            if chainID == cID {
                skipChain = false
            }
        }

        if skipChain {
            continue
        }

        contractAddresses := make([]gethcommon.Address, 0)
        for contractAddress := range erc721Tokens {
            contractAddresses = append(contractAddresses, gethcommon.HexToAddress(contractAddress))
        }

        if _, exists := ownedERC721Tokens[chainID]; !exists {
            ownedERC721Tokens[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
        }

        for _, owner := range walletAddresses {
            balances, err := getCollectiblesBalances(ctx, walletcommon.ChainID(chainID), owner, contractAddresses)
            if err != nil {
                p.logger.Info("couldn't fetch owner assets", zap.Error(err))
                return nil, err
            }
            ownedERC721Tokens[chainID][owner] = balances
        }
    }
    return ownedERC721Tokens, nil
}

func (p *DefaultPermissionChecker) accountChainsCombinationToMap(combinations []*AccountChainIDsCombination) map[gethcommon.Address][]uint64 {
    result := make(map[gethcommon.Address][]uint64)
    for _, combination := range combinations {
        result[combination.Address] = combination.ChainIDs
    }
    return result
}

// merge valid combinations w/o duplicates
func (p *DefaultPermissionChecker) MergeValidCombinations(left, right []*AccountChainIDsCombination) []*AccountChainIDsCombination {

    leftMap := p.accountChainsCombinationToMap(left)
    rightMap := p.accountChainsCombinationToMap(right)

    // merge maps, result in left map
    for k, v := range rightMap {
        if _, exists := leftMap[k]; !exists {
            leftMap[k] = v
            continue
        } else {
            // append chains which are new
            chains := leftMap[k]
            for _, chainID := range v {
                if !slices.Contains(chains, chainID) {
                    chains = append(chains, chainID)
                }
            }
            leftMap[k] = chains
        }
    }

    result := []*AccountChainIDsCombination{}
    for k, v := range leftMap {
        result = append(result, &AccountChainIDsCombination{
            Address:  k,
            ChainIDs: v,
        })
    }

    return result
}

func (p *DefaultPermissionChecker) CheckPermissionToJoin(community *Community, addresses []gethcommon.Address) (*CheckPermissionToJoinResponse, error) {
    becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN)
    becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER)
    becomeTokenMasterPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)

    adminOrTokenMasterPermissionsToJoin := append(becomeAdminPermissions, becomeTokenMasterPermissions...)

    allChainIDs, err := p.tokenManager.GetAllChainIDs()
    if err != nil {
        return nil, err
    }

    accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs)

    // Check becomeMember and (admin & token master) permissions separately.
    becomeMemberPermissionsResponse, err := p.checkPermissionsOrDefault(becomeMemberPermissions, accountsAndChainIDs)
    if err != nil {
        return nil, err
    }

    if len(adminOrTokenMasterPermissionsToJoin) <= 0 {
        return becomeMemberPermissionsResponse, nil
    }
    // If there are any admin or token master permissions, combine result.
    preParsedPermissions := preParsedCommunityPermissionsData(adminOrTokenMasterPermissionsToJoin)
    var adminOrTokenPermissionsResponse *CheckPermissionsResponse

    if community.IsControlNode() {
        adminOrTokenPermissionsResponse, err = p.CheckPermissions(preParsedPermissions, accountsAndChainIDs, false)
    } else {
        adminOrTokenPermissionsResponse, err = p.CheckCachedPermissions(preParsedPermissions, accountsAndChainIDs, false)
    }
    if err != nil {
        return nil, err
    }

    mergedPermissions := make(map[string]*PermissionTokenCriteriaResult)
    maps.Copy(mergedPermissions, becomeMemberPermissionsResponse.Permissions)
    maps.Copy(mergedPermissions, adminOrTokenPermissionsResponse.Permissions)

    mergedCombinations := p.MergeValidCombinations(becomeMemberPermissionsResponse.ValidCombinations, adminOrTokenPermissionsResponse.ValidCombinations)

    combinedResponse := &CheckPermissionsResponse{
        Satisfied:         becomeMemberPermissionsResponse.Satisfied || adminOrTokenPermissionsResponse.Satisfied,
        Permissions:       mergedPermissions,
        ValidCombinations: mergedCombinations,
    }

    return combinedResponse, nil
}

func (p *DefaultPermissionChecker) checkPermissionsOrDefault(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination) (*CheckPermissionsResponse, error) {
    if len(permissions) == 0 {
        // There are no permissions to join on this community at the moment,
        // so we reveal all accounts + all chain IDs
        response := &CheckPermissionsResponse{
            Satisfied:         true,
            Permissions:       make(map[string]*PermissionTokenCriteriaResult),
            ValidCombinations: accountsAndChainIDs,
        }
        return response, nil
    }

    preParsedPermissions := preParsedCommunityPermissionsData(permissions)
    return p.CheckCachedPermissions(preParsedPermissions, accountsAndChainIDs, false)
}

type ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error)
type balancesByChainGetter = func(ctx context.Context, accounts, tokens []gethcommon.Address, chainIDs []uint64) (BalancesByChain, error)

func (p *DefaultPermissionChecker) checkTokenRequirement(
    tokenRequirement *protobuf.TokenCriteria,
    accounts []gethcommon.Address, ownedERC20TokenBalances BalancesByChain, ownedERC721Tokens CollectiblesByChain,
    accountsChainIDsCombinations map[gethcommon.Address]map[uint64]bool,
) (TokenRequirementResponse, error) {
    tokenRequirementResponse := TokenRequirementResponse{TokenCriteria: tokenRequirement}

    switch tokenRequirement.Type {

    case protobuf.CommunityTokenType_ERC721:

        if len(ownedERC721Tokens) == 0 {
            return tokenRequirementResponse, nil
        }

        // Limit NFTs count to uint32
        requiredCount, err := strconv.ParseUint(tokenRequirement.AmountInWei, 10, 32)
        if err != nil {
            return tokenRequirementResponse, fmt.Errorf("invalid ERC721 amount: %s", tokenRequirement.AmountInWei)
        }
        accumulatedCount := uint64(0)

        for chainID, addressStr := range tokenRequirement.ContractAddresses {
            contractAddress := gethcommon.HexToAddress(addressStr)
            if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 {
                continue
            }

            for account := range ownedERC721Tokens[chainID] {
                if _, exists := ownedERC721Tokens[chainID][account]; !exists {
                    continue
                }

                tokenBalances := ownedERC721Tokens[chainID][account][contractAddress]
                accumulatedCount += uint64(len(tokenBalances))

                if len(tokenBalances) > 0 {
                    // 'account' owns some TokenID owned from contract 'address'
                    if _, exists := accountsChainIDsCombinations[account]; !exists {
                        accountsChainIDsCombinations[account] = make(map[uint64]bool)
                    }

                    // account has balance > 0 on this chain for this token, so let's add it the chain IDs
                    accountsChainIDsCombinations[account][chainID] = true

                    if len(tokenRequirement.TokenIds) == 0 {
                        // no specific tokenId of this collection is needed

                        if accumulatedCount >= requiredCount {
                            tokenRequirementResponse.Satisfied = true
                            return tokenRequirementResponse, nil
                        }
                    }

                    for _, tokenID := range tokenRequirement.TokenIds {
                        tokenIDBigInt := new(big.Int).SetUint64(tokenID)

                        for _, asset := range tokenBalances {
                            if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
                                tokenRequirementResponse.Satisfied = true
                                return tokenRequirementResponse, nil
                            }
                        }
                    }
                }
            }
        }

    case protobuf.CommunityTokenType_ERC20:

        if len(ownedERC20TokenBalances) == 0 {
            return tokenRequirementResponse, nil
        }

        accumulatedBalance := new(big.Int)

    chainIDLoopERC20:
        for chainID, address := range tokenRequirement.ContractAddresses {
            if _, exists := ownedERC20TokenBalances[chainID]; !exists || len(ownedERC20TokenBalances[chainID]) == 0 {
                continue chainIDLoopERC20
            }
            contractAddress := gethcommon.HexToAddress(address)
            for account := range ownedERC20TokenBalances[chainID] {
                if _, exists := ownedERC20TokenBalances[chainID][account][contractAddress]; !exists {
                    continue
                }

                value := ownedERC20TokenBalances[chainID][account][contractAddress]

                if _, exists := accountsChainIDsCombinations[account]; !exists {
                    accountsChainIDsCombinations[account] = make(map[uint64]bool)
                }

                if value.ToInt().Cmp(big.NewInt(0)) > 0 {
                    // account has balance > 0 on this chain for this token, so let's add it the chain IDs
                    accountsChainIDsCombinations[account][chainID] = true
                }

                // check if adding current chain account balance to accumulated balance
                // satisfies required amount
                prevBalance := accumulatedBalance
                accumulatedBalance.Add(prevBalance, value.ToInt())

                requiredAmount, success := new(big.Int).SetString(tokenRequirement.AmountInWei, 10)
                if !success {
                    return tokenRequirementResponse, fmt.Errorf("amountInWeis value is incorrect: %s", tokenRequirement.AmountInWei)
                }

                if accumulatedBalance.Cmp(requiredAmount) != -1 {
                    tokenRequirementResponse.Satisfied = true
                    return tokenRequirementResponse, nil
                }
            }
        }

    case protobuf.CommunityTokenType_ENS:

        for _, account := range accounts {
            ownedENSNames, err := p.getOwnedENS([]gethcommon.Address{account})
            if err != nil {
                return tokenRequirementResponse, err
            }

            if _, exists := accountsChainIDsCombinations[account]; !exists {
                accountsChainIDsCombinations[account] = make(map[uint64]bool)
            }

            if !strings.HasPrefix(tokenRequirement.EnsPattern, "*.") {
                for _, ownedENS := range ownedENSNames {
                    if ownedENS == tokenRequirement.EnsPattern {
                        accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
                        tokenRequirementResponse.Satisfied = true
                        return tokenRequirementResponse, nil
                    }
                }
            } else {
                parentName := tokenRequirement.EnsPattern[2:]
                for _, ownedENS := range ownedENSNames {
                    if strings.HasSuffix(ownedENS, parentName) {
                        accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true
                        tokenRequirementResponse.Satisfied = true
                        return tokenRequirementResponse, nil
                    }
                }
            }
        }

    }

    return tokenRequirementResponse, nil
}

func (p *DefaultPermissionChecker) checkPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool,
    getOwnedERC721Tokens ownedERC721TokensGetter, getBalancesByChain balancesByChainGetter) (*CheckPermissionsResponse, error) {

    response := &CheckPermissionsResponse{
        Satisfied:         false,
        Permissions:       make(map[string]*PermissionTokenCriteriaResult),
        ValidCombinations: make([]*AccountChainIDsCombination, 0),
    }

    if permissionsParsedData == nil {
        response.Satisfied = true
        return response, nil
    }

    erc721TokenRequirements := permissionsParsedData.Erc721TokenRequirements

    erc20ChainIDsMap := permissionsParsedData.Erc20ChainIDsMap
    erc721ChainIDsMap := permissionsParsedData.Erc721ChainIDsMap

    erc20TokenAddresses := permissionsParsedData.Erc20TokenAddresses

    accounts := make([]gethcommon.Address, 0)

    // TODO: move outside in order not to convert it
    for _, accountAndChainIDs := range accountsAndChainIDs {
        accounts = append(accounts, accountAndChainIDs.Address)
    }

    chainIDsForERC20 := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsMap)
    chainIDsForERC721 := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsMap)

    // if there are no chain IDs that match token criteria chain IDs
    // we aren't able to check balances on selected networks
    if len(erc20ChainIDsMap) > 0 && len(chainIDsForERC20) == 0 {
        response.NetworksNotSupported = true
        return response, nil
    }

    ownedERC20TokenBalances := make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, 0)
    if len(chainIDsForERC20) > 0 {
        // this only returns balances for the networks we're actually interested in
        balances, err := getBalancesByChain(context.Background(), accounts, erc20TokenAddresses, chainIDsForERC20)
        if err != nil {
            return nil, err
        }
        ownedERC20TokenBalances = balances
    }

    ownedERC721Tokens := make(CollectiblesByChain)
    if len(chainIDsForERC721) > 0 {
        collectibles, err := getOwnedERC721Tokens(accounts, erc721TokenRequirements, chainIDsForERC721)
        if err != nil {
            return nil, err
        }
        ownedERC721Tokens = collectibles
    }

    accountsChainIDsCombinations := make(map[gethcommon.Address]map[uint64]bool)

    for _, tokenPermission := range permissionsParsedData.Permissions {
        permissionRequirementsMet := true
        response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{Role: tokenPermission.Type}

        // There can be multiple token requirements per permission.
        // If only one is not met, the entire permission is marked
        // as not fulfilled
        for _, tokenRequirement := range tokenPermission.TokenCriteria {
            tokenRequirementResponse, err := p.checkTokenRequirement(tokenRequirement, accounts, ownedERC20TokenBalances, ownedERC721Tokens, accountsChainIDsCombinations)
            if err != nil {
                p.logger.Error("failed to check token requirement", zap.Error(err))
            }

            if !tokenRequirementResponse.Satisfied {
                permissionRequirementsMet = false
            }

            response.Permissions[tokenPermission.Id].TokenRequirements = append(response.Permissions[tokenPermission.Id].TokenRequirements, tokenRequirementResponse)
            response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, tokenRequirementResponse.Satisfied)
        }
        response.Permissions[tokenPermission.Id].ID = tokenPermission.Id

        // multiple permissions are treated as logical OR, meaning
        // if only one of them is fulfilled, the user gets permission
        // to join and we can stop early
        if shortcircuit && permissionRequirementsMet {
            break
        }
    }

    // attach valid account and chainID combinations to response
    for account, chainIDs := range accountsChainIDsCombinations {
        combination := &AccountChainIDsCombination{
            Address: account,
        }
        for chainID := range chainIDs {
            combination.ChainIDs = append(combination.ChainIDs, chainID)
        }
        response.ValidCombinations = append(response.ValidCombinations, combination)
    }

    response.calculateSatisfied()

    return response, nil
}

type balancesByOwnerAndContractAddressGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (map[gethcommon.Address][]thirdparty.TokenBalance, error)

func (p *DefaultPermissionChecker) handlePermissionsCheck(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool,
    getBalancesByOwnerAndContractAddress balancesByOwnerAndContractAddressGetter,
    getBalancesByChain balancesByChainGetter) (*CheckPermissionsResponse, error) {

    var getOwnedERC721Tokens ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
        return p.getOwnedERC721Tokens(walletAddresses, tokenRequirements, chainIDs, getBalancesByOwnerAndContractAddress)
    }

    return p.checkPermissions(permissionsParsedData, accountsAndChainIDs, shortcircuit, getOwnedERC721Tokens, getBalancesByChain)
}

func (p *DefaultPermissionChecker) CheckCachedPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
    return p.handlePermissionsCheck(permissionsParsedData, accountsAndChainIDs, shortcircuit, p.collectiblesManager.FetchCachedBalancesByOwnerAndContractAddress, p.tokenManager.GetCachedBalancesByChain)
}

// CheckPermissions will retrieve balances and check whether the user has
// permission to join the community, if shortcircuit is true, it will stop as soon
// as we know the answer
func (p *DefaultPermissionChecker) CheckPermissions(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) {
    return p.handlePermissionsCheck(permissionsParsedData, accountsAndChainIDs, shortcircuit, p.collectiblesManager.FetchBalancesByOwnerAndContractAddress, p.tokenManager.GetBalancesByChain)
}

type CollectiblesOwners = map[walletcommon.ChainID]map[gethcommon.Address]*thirdparty.CollectibleContractOwnership

// Same as CheckPermissions but relies on already provided collectibles owners
func (p *DefaultPermissionChecker) CheckPermissionsWithPreFetchedData(permissionsParsedData *PreParsedCommunityPermissionsData, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool, collectiblesOwners CollectiblesOwners) (*CheckPermissionsResponse, error) {
    var getCollectiblesBalances collectiblesBalancesGetter = func(ctx context.Context, chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
        ret := make(thirdparty.TokenBalancesPerContractAddress)

        collectiblesByChain, ok := collectiblesOwners[chainID]
        if !ok {
            return nil, errors.New("no data available for chainID")
        }

        for _, contractAddress := range contractAddresses {
            ownership, ok := collectiblesByChain[contractAddress]
            if !ok {
                return nil, errors.New("no data available for collectible")
            }

            for _, nftOwner := range ownership.Owners {
                if nftOwner.OwnerAddress == ownerAddress {
                    ret[contractAddress] = nftOwner.TokenBalances
                    break
                }
            }
        }

        return ret, nil
    }

    var getOwnedERC721Tokens ownedERC721TokensGetter = func(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
        return p.getOwnedERC721Tokens(walletAddresses, tokenRequirements, chainIDs, getCollectiblesBalances)
    }

    return p.checkPermissions(permissionsParsedData, accountsAndChainIDs, shortcircuit, getOwnedERC721Tokens, p.tokenManager.GetBalancesByChain)
}

func preParsedPermissionsData(permissions []*CommunityTokenPermission) *PreParsedPermissionsData {
    erc20TokenRequirements, erc721TokenRequirements, _ := ExtractTokenCriteria(permissions)

    erc20ChainIDsMap := make(map[uint64]bool)
    erc721ChainIDsMap := make(map[uint64]bool)

    erc20TokenAddresses := make([]gethcommon.Address, 0)

    // figure out chain IDs we're interested in
    for chainID, tokens := range erc20TokenRequirements {
        erc20ChainIDsMap[chainID] = true
        for contractAddress := range tokens {
            erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress))
        }
    }

    for chainID := range erc721TokenRequirements {
        erc721ChainIDsMap[chainID] = true
    }

    return &PreParsedPermissionsData{
        Erc721TokenRequirements: erc721TokenRequirements,
        Erc20TokenAddresses:     erc20TokenAddresses,
        Erc20ChainIDsMap:        erc20ChainIDsMap,
        Erc721ChainIDsMap:       erc721ChainIDsMap,
    }
}

func preParsedCommunityPermissionsData(permissions []*CommunityTokenPermission) *PreParsedCommunityPermissionsData {
    if len(permissions) == 0 {
        return nil
    }

    return &PreParsedCommunityPermissionsData{
        Permissions:              permissions,
        PreParsedPermissionsData: preParsedPermissionsData(permissions),
    }
}

func PreParsePermissionsData(permissions map[string]*CommunityTokenPermission) (map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData, map[string]*PreParsedCommunityPermissionsData) {
    becomeMemberPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_MEMBER)
    becomeAdminPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_ADMIN)
    becomeTokenMasterPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER)

    viewOnlyPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
    viewAndPostPermissions := TokenPermissionsByType(permissions, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
    channelPermissions := append(viewAndPostPermissions, viewOnlyPermissions...)

    communityPermissionsPreParsedData := make(map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData)
    communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_MEMBER] = preParsedCommunityPermissionsData(becomeMemberPermissions)
    communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_ADMIN] = preParsedCommunityPermissionsData(becomeAdminPermissions)
    communityPermissionsPreParsedData[protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER] = preParsedCommunityPermissionsData(becomeTokenMasterPermissions)

    channelPermissionsPreParsedData := make(map[string]*PreParsedCommunityPermissionsData)
    for _, channelPermission := range channelPermissions {
        channelPermissionsPreParsedData[channelPermission.Id] = preParsedCommunityPermissionsData([]*CommunityTokenPermission{channelPermission})
    }

    return communityPermissionsPreParsedData, channelPermissionsPreParsedData
}

func CollectibleAddressesFromPreParsedPermissionsData(communityPermissions map[protobuf.CommunityTokenPermission_Type]*PreParsedCommunityPermissionsData, channelPermissions map[string]*PreParsedCommunityPermissionsData) map[walletcommon.ChainID]map[gethcommon.Address]struct{} {
    ret := make(map[walletcommon.ChainID]map[gethcommon.Address]struct{})

    allPermissionsData := []*PreParsedCommunityPermissionsData{}
    for _, permissionsData := range communityPermissions {
        if permissionsData != nil {
            allPermissionsData = append(allPermissionsData, permissionsData)
        }
    }
    for _, permissionsData := range channelPermissions {
        if permissionsData != nil {
            allPermissionsData = append(allPermissionsData, permissionsData)
        }
    }

    for _, data := range allPermissionsData {
        for chainID, contractAddresses := range data.Erc721TokenRequirements {
            if ret[walletcommon.ChainID(chainID)] == nil {
                ret[walletcommon.ChainID(chainID)] = make(map[gethcommon.Address]struct{})
            }

            for contractAddress := range contractAddresses {
                ret[walletcommon.ChainID(chainID)][gethcommon.HexToAddress(contractAddress)] = struct{}{}
            }
        }
    }

    return ret
}