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 }