package communities import ( "context" "errors" "math" "math/big" "strconv" "strings" "go.uber.org/zap" 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(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) } type DefaultPermissionChecker struct { tokenManager TokenManager collectiblesManager CollectiblesManager ensVerifier *ens.Verifier logger *zap.Logger } 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 } func (p *DefaultPermissionChecker) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (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 := p.collectiblesManager.FetchBalancesByOwnerAndContractAddress(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) 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) permissionsToJoin := append(becomeAdminPermissions, becomeMemberPermissions...) permissionsToJoin = append(permissionsToJoin, becomeTokenMasterPermissions...) allChainIDs, err := p.tokenManager.GetAllChainIDs() if err != nil { return nil, err } accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs) if len(becomeMemberPermissions) == 0 || len(permissionsToJoin) == 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 } return p.CheckPermissions(permissionsToJoin, accountsAndChainIDs, false) } // 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(permissions []*CommunityTokenPermission, accountsAndChainIDs []*AccountChainIDsCombination, shortcircuit bool) (*CheckPermissionsResponse, error) { response := &CheckPermissionsResponse{ Satisfied: false, Permissions: make(map[string]*PermissionTokenCriteriaResult), ValidCombinations: make([]*AccountChainIDsCombination, 0), } erc20TokenRequirements, erc721TokenRequirements, _ := ExtractTokenCriteria(permissions) erc20ChainIDsMap := make(map[uint64]bool) erc721ChainIDsMap := make(map[uint64]bool) erc20TokenAddresses := make([]gethcommon.Address, 0) accounts := make([]gethcommon.Address, 0) for _, accountAndChainIDs := range accountsAndChainIDs { accounts = append(accounts, accountAndChainIDs.Address) } // 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 } 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 { 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 := p.tokenManager.GetBalancesByChain(context.Background(), accounts, erc20TokenAddresses, chainIDsForERC20) if err != nil { return nil, err } ownedERC20TokenBalances = balances } ownedERC721Tokens := make(CollectiblesByChain) if len(chainIDsForERC721) > 0 { collectibles, err := p.GetOwnedERC721Tokens(accounts, erc721TokenRequirements, chainIDsForERC721) if err != nil { return nil, err } ownedERC721Tokens = collectibles } accountsChainIDsCombinations := make(map[gethcommon.Address]map[uint64]bool) for _, tokenPermission := range permissions { permissionRequirementsMet := true response.Permissions[tokenPermission.Id] = &PermissionTokenCriteriaResult{} // 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 { tokenRequirementMet := false if tokenRequirement.Type == protobuf.CommunityTokenType_ERC721 { if len(ownedERC721Tokens) == 0 { response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false) continue } chainIDLoopERC721: for chainID, addressStr := range tokenRequirement.ContractAddresses { contractAddress := gethcommon.HexToAddress(addressStr) if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 { continue chainIDLoopERC721 } for account := range ownedERC721Tokens[chainID] { if _, exists := ownedERC721Tokens[chainID][account]; !exists { continue } tokenBalances := ownedERC721Tokens[chainID][account][contractAddress] if len(tokenBalances) > 0 { // 'account' owns some TokenID owned from contract 'address' if _, exists := accountsChainIDsCombinations[account]; !exists { accountsChainIDsCombinations[account] = make(map[uint64]bool) } if len(tokenRequirement.TokenIds) == 0 { // no specific tokenId of this collection is needed tokenRequirementMet = true accountsChainIDsCombinations[account][chainID] = true break chainIDLoopERC721 } tokenIDsLoop: 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 { tokenRequirementMet = true accountsChainIDsCombinations[account][chainID] = true break tokenIDsLoop } } } } } } } else if tokenRequirement.Type == protobuf.CommunityTokenType_ERC20 { if len(ownedERC20TokenBalances) == 0 { response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false) continue } accumulatedBalance := new(big.Float) 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] accountChainBalance := new(big.Float).Quo( new(big.Float).SetInt(value.ToInt()), big.NewFloat(math.Pow(10, float64(tokenRequirement.Decimals))), ) if _, exists := accountsChainIDsCombinations[account]; !exists { accountsChainIDsCombinations[account] = make(map[uint64]bool) } if accountChainBalance.Cmp(big.NewFloat(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, accountChainBalance) requiredAmount, err := strconv.ParseFloat(tokenRequirement.Amount, 32) if err != nil { return nil, err } if accumulatedBalance.Cmp(big.NewFloat(requiredAmount)) != -1 { tokenRequirementMet = true if shortcircuit { break chainIDLoopERC20 } } } } } else if tokenRequirement.Type == protobuf.CommunityTokenType_ENS { for _, account := range accounts { ownedENSNames, err := p.getOwnedENS([]gethcommon.Address{account}) if err != nil { return nil, 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 { tokenRequirementMet = true accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true } } } else { parentName := tokenRequirement.EnsPattern[2:] for _, ownedENS := range ownedENSNames { if strings.HasSuffix(ownedENS, parentName) { tokenRequirementMet = true accountsChainIDsCombinations[account][walletcommon.EthereumMainnet] = true } } } } } if !tokenRequirementMet { permissionRequirementsMet = false } response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, tokenRequirementMet) } // 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 }