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

     1  package communities
     2  
     3  import (
     4  	"context"
     5  	"math/big"
     6  	"strconv"
     7  
     8  	"github.com/pkg/errors"
     9  
    10  	gethcommon "github.com/ethereum/go-ethereum/common"
    11  	"github.com/status-im/status-go/eth-node/types"
    12  	"github.com/status-im/status-go/protocol/protobuf"
    13  	"github.com/status-im/status-go/services/wallet/bigint"
    14  	"github.com/status-im/status-go/services/wallet/thirdparty"
    15  )
    16  
    17  type PermissionedBalance struct {
    18  	Type     protobuf.CommunityTokenType `json:"type"`
    19  	Symbol   string                      `json:"symbol"`
    20  	Name     string                      `json:"name"`
    21  	Amount   *bigint.BigInt              `json:"amount"`
    22  	Decimals uint64                      `json:"decimals"`
    23  }
    24  
    25  func calculatePermissionedBalancesERC20(
    26  	accountAddresses []gethcommon.Address,
    27  	balances BalancesByChain,
    28  	tokenPermissions []*CommunityTokenPermission,
    29  ) map[gethcommon.Address]map[string]*PermissionedBalance {
    30  	res := make(map[gethcommon.Address]map[string]*PermissionedBalance)
    31  
    32  	// Set with composite key (chain ID + wallet address + contract address) to
    33  	// store if we already processed the balance.
    34  	usedBalances := make(map[string]bool)
    35  
    36  	for _, permission := range tokenPermissions {
    37  		for _, criteria := range permission.TokenCriteria {
    38  			if criteria.Type != protobuf.CommunityTokenType_ERC20 {
    39  				continue
    40  			}
    41  
    42  			for _, accountAddress := range accountAddresses {
    43  				for chainID, hexContractAddress := range criteria.ContractAddresses {
    44  					usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress
    45  
    46  					if _, ok := balances[chainID]; !ok {
    47  						continue
    48  					}
    49  					if _, ok := balances[chainID][accountAddress]; !ok {
    50  						continue
    51  					}
    52  
    53  					contractAddress := gethcommon.HexToAddress(hexContractAddress)
    54  					value, ok := balances[chainID][accountAddress][contractAddress]
    55  					if !ok {
    56  						continue
    57  					}
    58  
    59  					// Skip the contract address if it has been used already in the sum.
    60  					if _, ok := usedBalances[usedKey]; ok {
    61  						continue
    62  					}
    63  
    64  					if _, ok := res[accountAddress]; !ok {
    65  						res[accountAddress] = make(map[string]*PermissionedBalance, 0)
    66  					}
    67  					if _, ok := res[accountAddress][criteria.Symbol]; !ok {
    68  						res[accountAddress][criteria.Symbol] = &PermissionedBalance{
    69  							Type:     criteria.Type,
    70  							Symbol:   criteria.Symbol,
    71  							Name:     criteria.Name,
    72  							Decimals: criteria.Decimals,
    73  							Amount:   &bigint.BigInt{Int: big.NewInt(0)},
    74  						}
    75  					}
    76  
    77  					res[accountAddress][criteria.Symbol].Amount.Add(
    78  						res[accountAddress][criteria.Symbol].Amount.Int,
    79  						value.ToInt(),
    80  					)
    81  					usedBalances[usedKey] = true
    82  				}
    83  			}
    84  		}
    85  	}
    86  
    87  	return res
    88  }
    89  
    90  func isERC721CriteriaSatisfied(tokenBalances []thirdparty.TokenBalance, criteria *protobuf.TokenCriteria) bool {
    91  	// No token IDs to compare against, so the criteria is satisfied.
    92  	if len(criteria.TokenIds) == 0 {
    93  		return true
    94  	}
    95  
    96  	for _, tokenID := range criteria.TokenIds {
    97  		tokenIDBigInt := new(big.Int).SetUint64(tokenID)
    98  		for _, asset := range tokenBalances {
    99  			if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
   100  				return true
   101  			}
   102  		}
   103  	}
   104  
   105  	return false
   106  }
   107  
   108  func (m *Manager) calculatePermissionedBalancesERC721(
   109  	accountAddresses []gethcommon.Address,
   110  	balances CollectiblesByChain,
   111  	tokenPermissions []*CommunityTokenPermission,
   112  ) map[gethcommon.Address]map[string]*PermissionedBalance {
   113  	res := make(map[gethcommon.Address]map[string]*PermissionedBalance)
   114  
   115  	// Set with composite key (chain ID + wallet address + contract address) to
   116  	// store if we already processed the balance.
   117  	usedBalances := make(map[string]bool)
   118  
   119  	for _, permission := range tokenPermissions {
   120  		for _, criteria := range permission.TokenCriteria {
   121  			if criteria.Type != protobuf.CommunityTokenType_ERC721 {
   122  				continue
   123  			}
   124  
   125  			for _, accountAddress := range accountAddresses {
   126  				for chainID, hexContractAddress := range criteria.ContractAddresses {
   127  					usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress
   128  
   129  					if _, ok := balances[chainID]; !ok {
   130  						continue
   131  					}
   132  					if _, ok := balances[chainID][accountAddress]; !ok {
   133  						continue
   134  					}
   135  
   136  					contractAddress := gethcommon.HexToAddress(hexContractAddress)
   137  					tokenBalances, ok := balances[chainID][accountAddress][contractAddress]
   138  					if !ok || len(tokenBalances) == 0 {
   139  						continue
   140  					}
   141  
   142  					// Skip the contract address if it has been used already in the sum.
   143  					if _, ok := usedBalances[usedKey]; ok {
   144  						continue
   145  					}
   146  
   147  					usedBalances[usedKey] = true
   148  
   149  					if _, ok := res[accountAddress]; !ok {
   150  						res[accountAddress] = make(map[string]*PermissionedBalance, 0)
   151  					}
   152  					if _, ok := res[accountAddress][criteria.Symbol]; !ok {
   153  						res[accountAddress][criteria.Symbol] = &PermissionedBalance{
   154  							Type:     criteria.Type,
   155  							Symbol:   criteria.Symbol,
   156  							Name:     criteria.Name,
   157  							Decimals: criteria.Decimals,
   158  							Amount:   &bigint.BigInt{Int: big.NewInt(0)},
   159  						}
   160  					}
   161  
   162  					if isERC721CriteriaSatisfied(tokenBalances, criteria) {
   163  						// We don't care about summing balances, thus setting as 1 is
   164  						// sufficient.
   165  						res[accountAddress][criteria.Symbol].Amount = &bigint.BigInt{Int: big.NewInt(1)}
   166  					}
   167  				}
   168  			}
   169  		}
   170  	}
   171  
   172  	return res
   173  }
   174  
   175  func (m *Manager) calculatePermissionedBalances(
   176  	chainIDs []uint64,
   177  	accountAddresses []gethcommon.Address,
   178  	erc20Balances BalancesByChain,
   179  	erc721Balances CollectiblesByChain,
   180  	tokenPermissions []*CommunityTokenPermission,
   181  ) map[gethcommon.Address][]PermissionedBalance {
   182  	res := make(map[gethcommon.Address][]PermissionedBalance, 0)
   183  
   184  	aggregatedERC721Balances := m.calculatePermissionedBalancesERC721(accountAddresses, erc721Balances, tokenPermissions)
   185  	for accountAddress, tokens := range aggregatedERC721Balances {
   186  		for _, permissionedToken := range tokens {
   187  			if permissionedToken.Amount.Sign() > 0 {
   188  				res[accountAddress] = append(res[accountAddress], *permissionedToken)
   189  			}
   190  		}
   191  	}
   192  
   193  	aggregatedERC20Balances := calculatePermissionedBalancesERC20(accountAddresses, erc20Balances, tokenPermissions)
   194  	for accountAddress, tokens := range aggregatedERC20Balances {
   195  		for _, permissionedToken := range tokens {
   196  			if permissionedToken.Amount.Sign() > 0 {
   197  				res[accountAddress] = append(res[accountAddress], *permissionedToken)
   198  			}
   199  		}
   200  	}
   201  
   202  	return res
   203  }
   204  
   205  func keepRoleTokenPermissions(tokenPermissions map[string]*CommunityTokenPermission) []*CommunityTokenPermission {
   206  	res := make([]*CommunityTokenPermission, 0)
   207  	for _, p := range tokenPermissions {
   208  		if p.Type == protobuf.CommunityTokenPermission_BECOME_MEMBER ||
   209  			p.Type == protobuf.CommunityTokenPermission_BECOME_ADMIN ||
   210  			p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER ||
   211  			p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER {
   212  			res = append(res, p)
   213  		}
   214  	}
   215  	return res
   216  }
   217  
   218  // GetPermissionedBalances returns balances indexed by account address.
   219  //
   220  // It assumes balances in different chains with the same symbol can be summed.
   221  // It also assumes the criteria's decimals field is the same across different
   222  // criteria when they refer to the same asset (by symbol).
   223  func (m *Manager) GetPermissionedBalances(
   224  	ctx context.Context,
   225  	communityID types.HexBytes,
   226  	accountAddresses []gethcommon.Address,
   227  ) (map[gethcommon.Address][]PermissionedBalance, error) {
   228  	community, err := m.GetByID(communityID)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	if community == nil {
   233  		return nil, errors.Errorf("community does not exist ID='%s'", communityID)
   234  	}
   235  
   236  	tokenPermissions := keepRoleTokenPermissions(community.TokenPermissions())
   237  
   238  	allChainIDs, err := m.tokenManager.GetAllChainIDs()
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	accountsAndChainIDs := combineAddressesAndChainIDs(accountAddresses, allChainIDs)
   243  
   244  	erc20TokenCriteriaByChain, erc721TokenCriteriaByChain, _ := ExtractTokenCriteria(tokenPermissions)
   245  
   246  	accounts := make([]gethcommon.Address, 0, len(accountsAndChainIDs))
   247  	for _, accountAndChainIDs := range accountsAndChainIDs {
   248  		accounts = append(accounts, accountAndChainIDs.Address)
   249  	}
   250  
   251  	erc20ChainIDsSet := make(map[uint64]bool)
   252  	erc20TokenAddresses := make([]gethcommon.Address, 0)
   253  	for chainID, criterionByContractAddress := range erc20TokenCriteriaByChain {
   254  		erc20ChainIDsSet[chainID] = true
   255  		for contractAddress := range criterionByContractAddress {
   256  			erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress))
   257  		}
   258  	}
   259  
   260  	erc721ChainIDsSet := make(map[uint64]bool)
   261  	for chainID := range erc721TokenCriteriaByChain {
   262  		erc721ChainIDsSet[chainID] = true
   263  	}
   264  
   265  	erc20ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsSet)
   266  	erc721ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsSet)
   267  
   268  	erc20Balances, err := m.tokenManager.GetBalancesByChain(ctx, accounts, erc20TokenAddresses, erc20ChainIDs)
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  
   273  	erc721Balances := make(CollectiblesByChain)
   274  	if len(erc721ChainIDs) > 0 {
   275  		balances, err := m.GetOwnedERC721Tokens(accounts, erc721TokenCriteriaByChain, erc721ChainIDs)
   276  		if err != nil {
   277  			return nil, err
   278  		}
   279  
   280  		erc721Balances = balances
   281  	}
   282  
   283  	return m.calculatePermissionedBalances(allChainIDs, accountAddresses, erc20Balances, erc721Balances, tokenPermissions), nil
   284  }