From b727f1e14bdd1849d8861b9dd26a2d34f370c92d Mon Sep 17 00:00:00 2001 From: Icaro Motta Date: Mon, 5 Feb 2024 22:09:08 -0300 Subject: [PATCH] Extract entire permissioned balances logic to separate file --- protocol/communities/manager.go | 264 --------------- protocol/communities/manager_test.go | 303 ----------------- protocol/communities/permissioned_balances.go | 278 +++++++++++++++ .../communities/permissioned_balances_test.go | 319 ++++++++++++++++++ 4 files changed, 597 insertions(+), 567 deletions(-) create mode 100644 protocol/communities/permissioned_balances.go create mode 100644 protocol/communities/permissioned_balances_test.go diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index f06ece424..2afaca9e5 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "math/big" "net" "os" "sort" @@ -2185,269 +2184,6 @@ func (m *Manager) CheckPermissionToJoin(id []byte, addresses []gethcommon.Addres } -type PermissionedBalance struct { - Type protobuf.CommunityTokenType `json:"type"` - Symbol string `json:"symbol"` - Name string `json:"name"` - Amount *bigint.BigInt `json:"amount"` - Decimals uint64 `json:"decimals"` -} - -func calculatePermissionedBalancesERC20( - accountAddresses []gethcommon.Address, - balances BalancesByChain, - tokenPermissions []*CommunityTokenPermission, -) map[gethcommon.Address]map[string]*PermissionedBalance { - res := make(map[gethcommon.Address]map[string]*PermissionedBalance) - - // Set with composite key (chain ID + wallet address + contract address) to - // store if we already processed the balance. - usedBalances := make(map[string]bool) - - for _, permission := range tokenPermissions { - for _, criteria := range permission.TokenCriteria { - if criteria.Type != protobuf.CommunityTokenType_ERC20 { - continue - } - - for _, accountAddress := range accountAddresses { - for chainID, hexContractAddress := range criteria.ContractAddresses { - usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress - - if _, ok := balances[chainID]; !ok { - continue - } - if _, ok := balances[chainID][accountAddress]; !ok { - continue - } - - contractAddress := gethcommon.HexToAddress(hexContractAddress) - value, ok := balances[chainID][accountAddress][contractAddress] - if !ok { - continue - } - - // Skip the contract address if it has been used already in the sum. - if _, ok := usedBalances[usedKey]; ok { - continue - } - - if _, ok := res[accountAddress]; !ok { - res[accountAddress] = make(map[string]*PermissionedBalance, 0) - } - if _, ok := res[accountAddress][criteria.Symbol]; !ok { - res[accountAddress][criteria.Symbol] = &PermissionedBalance{ - Type: criteria.Type, - Symbol: criteria.Symbol, - Name: criteria.Name, - Decimals: criteria.Decimals, - Amount: &bigint.BigInt{Int: big.NewInt(0)}, - } - } - - res[accountAddress][criteria.Symbol].Amount.Add( - res[accountAddress][criteria.Symbol].Amount.Int, - value.ToInt(), - ) - usedBalances[usedKey] = true - } - } - } - } - - return res -} - -func isERC721CriteriaSatisfied(tokenBalances []thirdparty.TokenBalance, criteria *protobuf.TokenCriteria) bool { - for _, tokenID := range criteria.TokenIds { - tokenIDBigInt := new(big.Int).SetUint64(tokenID) - for _, asset := range tokenBalances { - if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 { - return true - } - } - } - return false -} - -func calculatePermissionedBalancesERC721( - accountAddresses []gethcommon.Address, - balances CollectiblesByChain, - tokenPermissions []*CommunityTokenPermission, -) map[gethcommon.Address]map[string]*PermissionedBalance { - res := make(map[gethcommon.Address]map[string]*PermissionedBalance) - - // Set with composite key (chain ID + wallet address + contract address) to - // store if we already processed the balance. - usedBalances := make(map[string]bool) - - for _, permission := range tokenPermissions { - for _, criteria := range permission.TokenCriteria { - if criteria.Type != protobuf.CommunityTokenType_ERC721 { - continue - } - - for _, accountAddress := range accountAddresses { - for chainID, hexContractAddress := range criteria.ContractAddresses { - usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress - - if _, ok := balances[chainID]; !ok { - continue - } - if _, ok := balances[chainID][accountAddress]; !ok { - continue - } - - contractAddress := gethcommon.HexToAddress(hexContractAddress) - tokenBalances, ok := balances[chainID][accountAddress][contractAddress] - if !ok || len(tokenBalances) == 0 { - continue - } - - // Skip the contract address if it has been used already in the sum. - if _, ok := usedBalances[usedKey]; ok { - continue - } - - usedBalances[usedKey] = true - - if _, ok := res[accountAddress]; !ok { - res[accountAddress] = make(map[string]*PermissionedBalance, 0) - } - if _, ok := res[accountAddress][criteria.Symbol]; !ok { - res[accountAddress][criteria.Symbol] = &PermissionedBalance{ - Type: criteria.Type, - Symbol: criteria.Symbol, - Name: criteria.Name, - Decimals: criteria.Decimals, - Amount: &bigint.BigInt{Int: big.NewInt(0)}, - } - } - - if isERC721CriteriaSatisfied(tokenBalances, criteria) { - // We don't care about summing balances, thus setting as 1 is - // sufficient. - res[accountAddress][criteria.Symbol].Amount = &bigint.BigInt{Int: big.NewInt(1)} - } - } - } - } - } - - return res -} - -func calculatePermissionedBalances( - chainIDs []uint64, - accountAddresses []gethcommon.Address, - erc20Balances BalancesByChain, - erc721Balances CollectiblesByChain, - tokenPermissions []*CommunityTokenPermission, -) map[gethcommon.Address][]PermissionedBalance { - res := make(map[gethcommon.Address][]PermissionedBalance, 0) - - aggregatedERC721Balances := calculatePermissionedBalancesERC721(accountAddresses, erc721Balances, tokenPermissions) - for accountAddress, tokens := range aggregatedERC721Balances { - for _, permissionedToken := range tokens { - if permissionedToken.Amount.Sign() > 0 { - res[accountAddress] = append(res[accountAddress], *permissionedToken) - } - } - } - - aggregatedERC20Balances := calculatePermissionedBalancesERC20(accountAddresses, erc20Balances, tokenPermissions) - for accountAddress, tokens := range aggregatedERC20Balances { - for _, permissionedToken := range tokens { - if permissionedToken.Amount.Sign() > 0 { - res[accountAddress] = append(res[accountAddress], *permissionedToken) - } - } - } - - return res -} - -func keepRoleTokenPermissions(tokenPermissions map[string]*CommunityTokenPermission) []*CommunityTokenPermission { - res := make([]*CommunityTokenPermission, 0) - for _, p := range tokenPermissions { - if p.Type == protobuf.CommunityTokenPermission_BECOME_MEMBER || - p.Type == protobuf.CommunityTokenPermission_BECOME_ADMIN || - p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER || - p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER { - res = append(res, p) - } - } - return res -} - -// GetPermissionedBalances returns balances indexed by account address. -// -// It assumes balances in different chains with the same symbol can be summed. -// It also assumes the criteria's decimals field is the same across different -// criteria when they refer to the same asset (by symbol). -func (m *Manager) GetPermissionedBalances( - ctx context.Context, - communityID types.HexBytes, - accountAddresses []gethcommon.Address, -) (map[gethcommon.Address][]PermissionedBalance, error) { - community, err := m.GetByID(communityID) - if err != nil { - return nil, err - } - if community == nil { - return nil, errors.Errorf("community does not exist ID='%s'", communityID) - } - - tokenPermissions := keepRoleTokenPermissions(community.TokenPermissions()) - - allChainIDs, err := m.tokenManager.GetAllChainIDs() - if err != nil { - return nil, err - } - accountsAndChainIDs := combineAddressesAndChainIDs(accountAddresses, allChainIDs) - - erc20TokenCriteriaByChain, erc721TokenCriteriaByChain, _ := ExtractTokenCriteria(tokenPermissions) - - accounts := make([]gethcommon.Address, 0, len(accountsAndChainIDs)) - for _, accountAndChainIDs := range accountsAndChainIDs { - accounts = append(accounts, accountAndChainIDs.Address) - } - - erc20ChainIDsSet := make(map[uint64]bool) - erc20TokenAddresses := make([]gethcommon.Address, 0) - for chainID, criterionByContractAddress := range erc20TokenCriteriaByChain { - erc20ChainIDsSet[chainID] = true - for contractAddress := range criterionByContractAddress { - erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress)) - } - } - - erc721ChainIDsSet := make(map[uint64]bool) - for chainID := range erc721TokenCriteriaByChain { - erc721ChainIDsSet[chainID] = true - } - - erc20ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsSet) - erc721ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsSet) - - erc20Balances, err := m.tokenManager.GetBalancesByChain(ctx, accounts, erc20TokenAddresses, erc20ChainIDs) - if err != nil { - return nil, err - } - - erc721Balances := make(CollectiblesByChain) - if len(erc721ChainIDs) > 0 { - balances, err := m.GetOwnedERC721Tokens(accounts, erc721TokenCriteriaByChain, erc721ChainIDs) - if err != nil { - return nil, err - } - - erc721Balances = balances - } - - return calculatePermissionedBalances(allChainIDs, accountAddresses, erc20Balances, erc721Balances, tokenPermissions), nil -} - func (m *Manager) accountsSatisfyPermissionsToJoin(community *Community, accounts []*protobuf.RevealedAccount) (bool, protobuf.CommunityMember_Roles, error) { accountsAndChainIDs := revealedAccountsToAccountsAndChainIDsCombination(accounts) becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN) diff --git a/protocol/communities/manager_test.go b/protocol/communities/manager_test.go index 0b7a634c0..65f4913a0 100644 --- a/protocol/communities/manager_test.go +++ b/protocol/communities/manager_test.go @@ -226,309 +226,6 @@ func (s *ManagerSuite) TestRetrieveTokens() { s.Require().False(resp.Satisfied) } -func (s *ManagerSuite) Test_calculatePermissionedBalances() { - var mainnetID uint64 = 1 - var arbitrumID uint64 = 42161 - var gnosisID uint64 = 100 - chainIDs := []uint64{mainnetID, arbitrumID} - - mainnetSNTContractAddress := gethcommon.HexToAddress("0xC") - mainnetETHContractAddress := gethcommon.HexToAddress("0xA") - arbitrumETHContractAddress := gethcommon.HexToAddress("0xB") - mainnetTMasterAddress := gethcommon.HexToAddress("0x123") - mainnetOwnerAddress := gethcommon.HexToAddress("0x1234") - - account1Address := gethcommon.HexToAddress("0x1") - account2Address := gethcommon.HexToAddress("0x2") - account3Address := gethcommon.HexToAddress("0x3") - accountAddresses := []gethcommon.Address{account1Address, account2Address, account3Address} - - erc20Balances := make(BalancesByChain) - erc721Balances := make(CollectiblesByChain) - - erc20Balances[mainnetID] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) - erc20Balances[arbitrumID] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) - erc20Balances[gnosisID] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) - erc721Balances[mainnetID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress) - - // Account 1 balances - erc20Balances[mainnetID][account1Address] = make(map[gethcommon.Address]*hexutil.Big) - erc20Balances[mainnetID][account1Address][mainnetETHContractAddress] = intToBig(10) - erc20Balances[arbitrumID][account1Address] = make(map[gethcommon.Address]*hexutil.Big) - erc20Balances[arbitrumID][account1Address][arbitrumETHContractAddress] = intToBig(25) - - // Account 2 balances - erc20Balances[mainnetID][account2Address] = make(map[gethcommon.Address]*hexutil.Big) - erc20Balances[mainnetID][account2Address][mainnetSNTContractAddress] = intToBig(120) - erc721Balances[mainnetID][account2Address] = make(thirdparty.TokenBalancesPerContractAddress) - erc721Balances[mainnetID][account2Address][mainnetTMasterAddress] = []thirdparty.TokenBalance{ - thirdparty.TokenBalance{ - TokenID: uintToDecBig(456), - Balance: uintToDecBig(1), - }, - thirdparty.TokenBalance{ - TokenID: uintToDecBig(123), - Balance: uintToDecBig(2), - }, - } - erc721Balances[mainnetID][account2Address][mainnetOwnerAddress] = []thirdparty.TokenBalance{ - thirdparty.TokenBalance{ - TokenID: uintToDecBig(100), - Balance: uintToDecBig(6), - }, - thirdparty.TokenBalance{ - TokenID: uintToDecBig(101), - Balance: uintToDecBig(1), - }, - } - - erc20Balances[arbitrumID][account2Address] = make(map[gethcommon.Address]*hexutil.Big) - erc20Balances[arbitrumID][account2Address][arbitrumETHContractAddress] = intToBig(2) - - // Account 3 balances. This account is used to assert zeroed balances are - // removed from the final response. - erc20Balances[mainnetID][account3Address] = make(map[gethcommon.Address]*hexutil.Big) - erc20Balances[mainnetID][account3Address][mainnetETHContractAddress] = intToBig(0) - - // A balance that should be ignored because the list of wallet addresses don't - // contain any wallet in the Gnosis chain. - erc20Balances[gnosisID][gethcommon.HexToAddress("0xF")] = make(map[gethcommon.Address]*hexutil.Big) - erc20Balances[gnosisID][gethcommon.HexToAddress("0xF")][gethcommon.HexToAddress("0x99")] = intToBig(5) - - tokenPermissions := []*CommunityTokenPermission{ - &CommunityTokenPermission{ - CommunityTokenPermission: &protobuf.CommunityTokenPermission{ - Type: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC721, - Symbol: "TMTEST", - Name: "TMaster-Test", - Amount: "1", - TokenIds: []uint64{123, 456}, - ContractAddresses: map[uint64]string{mainnetID: mainnetTMasterAddress.Hex()}, - }, - }, - }, - }, - &CommunityTokenPermission{ - CommunityTokenPermission: &protobuf.CommunityTokenPermission{ - Type: protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC721, - Symbol: "OWNTEST", - Name: "Owner-Test", - Amount: "5", - // No account has a positive balance for these token IDs, so we - // expect this collectible to not be present in the final result. - TokenIds: []uint64{666}, - ContractAddresses: map[uint64]string{mainnetID: mainnetOwnerAddress.Hex()}, - }, - }, - }, - }, - &CommunityTokenPermission{ - CommunityTokenPermission: &protobuf.CommunityTokenPermission{ - Type: protobuf.CommunityTokenPermission_BECOME_ADMIN, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "ETH", - Name: "Ethereum", - Amount: "20", - Decimals: 18, - ContractAddresses: map[uint64]string{ - arbitrumID: arbitrumETHContractAddress.Hex(), - mainnetID: mainnetETHContractAddress.Hex(), - }, - }, - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "ETH", - Name: "Ethereum", - Amount: "4", - Decimals: 18, - ContractAddresses: map[uint64]string{arbitrumID: arbitrumETHContractAddress.Hex()}, - }, - }, - }, - }, - &CommunityTokenPermission{ - CommunityTokenPermission: &protobuf.CommunityTokenPermission{ - Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "SNT", - Name: "Status", - Amount: "1000", - Decimals: 16, - ContractAddresses: map[uint64]string{mainnetID: mainnetSNTContractAddress.Hex()}, - }, - }, - }, - }, - &CommunityTokenPermission{ - CommunityTokenPermission: &protobuf.CommunityTokenPermission{ - // Unknown permission should be ignored. - Type: protobuf.CommunityTokenPermission_UNKNOWN_TOKEN_PERMISSION, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "DAI", - Name: "Dai", - Amount: "7", - Decimals: 12, - ContractAddresses: map[uint64]string{mainnetID: "0x1234567"}, - }, - }, - }, - }, - } - - actual := calculatePermissionedBalances( - chainIDs, - accountAddresses, - erc20Balances, - erc721Balances, - tokenPermissions, - ) - - expected := make(map[gethcommon.Address][]PermissionedBalance) - expected[account1Address] = []PermissionedBalance{ - PermissionedBalance{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "ETH", - Name: "Ethereum", - Decimals: 18, - Amount: &bigint.BigInt{Int: big.NewInt(35)}, - }, - } - expected[account2Address] = []PermissionedBalance{ - PermissionedBalance{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "ETH", - Name: "Ethereum", - Decimals: 18, - Amount: &bigint.BigInt{Int: big.NewInt(2)}, - }, - PermissionedBalance{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "SNT", - Name: "Status", - Decimals: 16, - Amount: &bigint.BigInt{Int: big.NewInt(120)}, - }, - PermissionedBalance{ - Type: protobuf.CommunityTokenType_ERC721, - Symbol: "TMTEST", - Name: "TMaster-Test", - Amount: &bigint.BigInt{Int: big.NewInt(1)}, - }, - } - - _, ok := actual[account1Address] - s.Require().True(ok, "not found account1Address='%s'", account1Address) - _, ok = actual[account1Address] - s.Require().True(ok, "not found account2Address='%s'", account2Address) - - for accountAddress, permissionedTokens := range actual { - s.Require().ElementsMatch(expected[accountAddress], permissionedTokens, "accountAddress='%s'", accountAddress) - } -} - -func (s *ManagerSuite) Test_GetPermissionedBalances() { - m, collectiblesManager, tokenManager := s.setupManagerForTokenPermissions() - s.Require().NotNil(m) - s.Require().NotNil(collectiblesManager) - - request := &requests.CreateCommunity{ - Membership: protobuf.CommunityPermissions_AUTO_ACCEPT, - } - community, err := m.CreateCommunity(request, true) - s.Require().NoError(err) - s.Require().NotNil(community) - - accountAddress := gethcommon.HexToAddress("0x1") - accountAddresses := []gethcommon.Address{accountAddress} - - var chainID uint64 = 5 - erc20ETHAddress := gethcommon.HexToAddress("0xA") - erc721Address := gethcommon.HexToAddress("0x123") - - permissionRequest := &requests.CreateCommunityTokenPermission{ - CommunityID: community.ID(), - Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "ETH", - Name: "Ethereum", - Amount: "3", - Decimals: 18, - ContractAddresses: map[uint64]string{chainID: erc20ETHAddress.Hex()}, - }, - }, - } - _, changes, err := m.CreateCommunityTokenPermission(permissionRequest) - s.Require().NoError(err) - s.Require().Len(changes.TokenPermissionsAdded, 1) - - permissionRequest = &requests.CreateCommunityTokenPermission{ - CommunityID: community.ID(), - Type: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, - TokenCriteria: []*protobuf.TokenCriteria{ - &protobuf.TokenCriteria{ - Type: protobuf.CommunityTokenType_ERC721, - Symbol: "TMTEST", - Name: "TMaster-Test", - Amount: "1", - TokenIds: []uint64{666}, - ContractAddresses: map[uint64]string{chainID: erc721Address.Hex()}, - }, - }, - } - _, changes, err = m.CreateCommunityTokenPermission(permissionRequest) - s.Require().NoError(err) - s.Require().Len(changes.TokenPermissionsAdded, 1) - - tokenManager.setResponse(chainID, accountAddress, erc20ETHAddress, 42) - collectiblesManager.setResponse(chainID, accountAddress, erc721Address, []thirdparty.TokenBalance{ - thirdparty.TokenBalance{ - TokenID: uintToDecBig(666), - Balance: uintToDecBig(15), - }, - }) - - actual, err := m.GetPermissionedBalances(context.Background(), community.ID(), accountAddresses) - s.Require().NoError(err) - - expected := make(map[gethcommon.Address][]PermissionedBalance) - expected[accountAddress] = []PermissionedBalance{ - PermissionedBalance{ - Type: protobuf.CommunityTokenType_ERC20, - Symbol: "ETH", - Name: "Ethereum", - Decimals: 18, - Amount: &bigint.BigInt{Int: big.NewInt(42)}, - }, - PermissionedBalance{ - Type: protobuf.CommunityTokenType_ERC721, - Symbol: "TMTEST", - Name: "TMaster-Test", - Amount: &bigint.BigInt{Int: big.NewInt(1)}, - }, - } - - _, ok := actual[accountAddress] - s.Require().True(ok, "not found accountAddress='%s'", accountAddress) - - for address, permissionedBalances := range actual { - s.Require().ElementsMatch(expected[address], permissionedBalances) - } -} - func (s *ManagerSuite) TestRetrieveCollectibles() { m, cm, _ := s.setupManagerForTokenPermissions() diff --git a/protocol/communities/permissioned_balances.go b/protocol/communities/permissioned_balances.go new file mode 100644 index 000000000..2c8fab177 --- /dev/null +++ b/protocol/communities/permissioned_balances.go @@ -0,0 +1,278 @@ +package communities + +import ( + "context" + "math/big" + "strconv" + + "github.com/pkg/errors" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/services/wallet/bigint" + "github.com/status-im/status-go/services/wallet/thirdparty" +) + +type PermissionedBalance struct { + Type protobuf.CommunityTokenType `json:"type"` + Symbol string `json:"symbol"` + Name string `json:"name"` + Amount *bigint.BigInt `json:"amount"` + Decimals uint64 `json:"decimals"` +} + +func calculatePermissionedBalancesERC20( + accountAddresses []gethcommon.Address, + balances BalancesByChain, + tokenPermissions []*CommunityTokenPermission, +) map[gethcommon.Address]map[string]*PermissionedBalance { + res := make(map[gethcommon.Address]map[string]*PermissionedBalance) + + // Set with composite key (chain ID + wallet address + contract address) to + // store if we already processed the balance. + usedBalances := make(map[string]bool) + + for _, permission := range tokenPermissions { + for _, criteria := range permission.TokenCriteria { + if criteria.Type != protobuf.CommunityTokenType_ERC20 { + continue + } + + for _, accountAddress := range accountAddresses { + for chainID, hexContractAddress := range criteria.ContractAddresses { + usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress + + if _, ok := balances[chainID]; !ok { + continue + } + if _, ok := balances[chainID][accountAddress]; !ok { + continue + } + + contractAddress := gethcommon.HexToAddress(hexContractAddress) + value, ok := balances[chainID][accountAddress][contractAddress] + if !ok { + continue + } + + // Skip the contract address if it has been used already in the sum. + if _, ok := usedBalances[usedKey]; ok { + continue + } + + if _, ok := res[accountAddress]; !ok { + res[accountAddress] = make(map[string]*PermissionedBalance, 0) + } + if _, ok := res[accountAddress][criteria.Symbol]; !ok { + res[accountAddress][criteria.Symbol] = &PermissionedBalance{ + Type: criteria.Type, + Symbol: criteria.Symbol, + Name: criteria.Name, + Decimals: criteria.Decimals, + Amount: &bigint.BigInt{Int: big.NewInt(0)}, + } + } + + res[accountAddress][criteria.Symbol].Amount.Add( + res[accountAddress][criteria.Symbol].Amount.Int, + value.ToInt(), + ) + usedBalances[usedKey] = true + } + } + } + } + + return res +} + +func isERC721CriteriaSatisfied(tokenBalances []thirdparty.TokenBalance, criteria *protobuf.TokenCriteria) bool { + for _, tokenID := range criteria.TokenIds { + tokenIDBigInt := new(big.Int).SetUint64(tokenID) + for _, asset := range tokenBalances { + if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 { + return true + } + } + } + return false +} + +func calculatePermissionedBalancesERC721( + accountAddresses []gethcommon.Address, + balances CollectiblesByChain, + tokenPermissions []*CommunityTokenPermission, +) map[gethcommon.Address]map[string]*PermissionedBalance { + res := make(map[gethcommon.Address]map[string]*PermissionedBalance) + + // Set with composite key (chain ID + wallet address + contract address) to + // store if we already processed the balance. + usedBalances := make(map[string]bool) + + for _, permission := range tokenPermissions { + for _, criteria := range permission.TokenCriteria { + if criteria.Type != protobuf.CommunityTokenType_ERC721 { + continue + } + + for _, accountAddress := range accountAddresses { + for chainID, hexContractAddress := range criteria.ContractAddresses { + usedKey := strconv.FormatUint(chainID, 10) + "-" + accountAddress.Hex() + "-" + hexContractAddress + + if _, ok := balances[chainID]; !ok { + continue + } + if _, ok := balances[chainID][accountAddress]; !ok { + continue + } + + contractAddress := gethcommon.HexToAddress(hexContractAddress) + tokenBalances, ok := balances[chainID][accountAddress][contractAddress] + if !ok || len(tokenBalances) == 0 { + continue + } + + // Skip the contract address if it has been used already in the sum. + if _, ok := usedBalances[usedKey]; ok { + continue + } + + usedBalances[usedKey] = true + + if _, ok := res[accountAddress]; !ok { + res[accountAddress] = make(map[string]*PermissionedBalance, 0) + } + if _, ok := res[accountAddress][criteria.Symbol]; !ok { + res[accountAddress][criteria.Symbol] = &PermissionedBalance{ + Type: criteria.Type, + Symbol: criteria.Symbol, + Name: criteria.Name, + Decimals: criteria.Decimals, + Amount: &bigint.BigInt{Int: big.NewInt(0)}, + } + } + + if isERC721CriteriaSatisfied(tokenBalances, criteria) { + // We don't care about summing balances, thus setting as 1 is + // sufficient. + res[accountAddress][criteria.Symbol].Amount = &bigint.BigInt{Int: big.NewInt(1)} + } + } + } + } + } + + return res +} + +func calculatePermissionedBalances( + chainIDs []uint64, + accountAddresses []gethcommon.Address, + erc20Balances BalancesByChain, + erc721Balances CollectiblesByChain, + tokenPermissions []*CommunityTokenPermission, +) map[gethcommon.Address][]PermissionedBalance { + res := make(map[gethcommon.Address][]PermissionedBalance, 0) + + aggregatedERC721Balances := calculatePermissionedBalancesERC721(accountAddresses, erc721Balances, tokenPermissions) + for accountAddress, tokens := range aggregatedERC721Balances { + for _, permissionedToken := range tokens { + if permissionedToken.Amount.Sign() > 0 { + res[accountAddress] = append(res[accountAddress], *permissionedToken) + } + } + } + + aggregatedERC20Balances := calculatePermissionedBalancesERC20(accountAddresses, erc20Balances, tokenPermissions) + for accountAddress, tokens := range aggregatedERC20Balances { + for _, permissionedToken := range tokens { + if permissionedToken.Amount.Sign() > 0 { + res[accountAddress] = append(res[accountAddress], *permissionedToken) + } + } + } + + return res +} + +func keepRoleTokenPermissions(tokenPermissions map[string]*CommunityTokenPermission) []*CommunityTokenPermission { + res := make([]*CommunityTokenPermission, 0) + for _, p := range tokenPermissions { + if p.Type == protobuf.CommunityTokenPermission_BECOME_MEMBER || + p.Type == protobuf.CommunityTokenPermission_BECOME_ADMIN || + p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER || + p.Type == protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER { + res = append(res, p) + } + } + return res +} + +// GetPermissionedBalances returns balances indexed by account address. +// +// It assumes balances in different chains with the same symbol can be summed. +// It also assumes the criteria's decimals field is the same across different +// criteria when they refer to the same asset (by symbol). +func (m *Manager) GetPermissionedBalances( + ctx context.Context, + communityID types.HexBytes, + accountAddresses []gethcommon.Address, +) (map[gethcommon.Address][]PermissionedBalance, error) { + community, err := m.GetByID(communityID) + if err != nil { + return nil, err + } + if community == nil { + return nil, errors.Errorf("community does not exist ID='%s'", communityID) + } + + tokenPermissions := keepRoleTokenPermissions(community.TokenPermissions()) + + allChainIDs, err := m.tokenManager.GetAllChainIDs() + if err != nil { + return nil, err + } + accountsAndChainIDs := combineAddressesAndChainIDs(accountAddresses, allChainIDs) + + erc20TokenCriteriaByChain, erc721TokenCriteriaByChain, _ := ExtractTokenCriteria(tokenPermissions) + + accounts := make([]gethcommon.Address, 0, len(accountsAndChainIDs)) + for _, accountAndChainIDs := range accountsAndChainIDs { + accounts = append(accounts, accountAndChainIDs.Address) + } + + erc20ChainIDsSet := make(map[uint64]bool) + erc20TokenAddresses := make([]gethcommon.Address, 0) + for chainID, criterionByContractAddress := range erc20TokenCriteriaByChain { + erc20ChainIDsSet[chainID] = true + for contractAddress := range criterionByContractAddress { + erc20TokenAddresses = append(erc20TokenAddresses, gethcommon.HexToAddress(contractAddress)) + } + } + + erc721ChainIDsSet := make(map[uint64]bool) + for chainID := range erc721TokenCriteriaByChain { + erc721ChainIDsSet[chainID] = true + } + + erc20ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc20ChainIDsSet) + erc721ChainIDs := calculateChainIDsSet(accountsAndChainIDs, erc721ChainIDsSet) + + erc20Balances, err := m.tokenManager.GetBalancesByChain(ctx, accounts, erc20TokenAddresses, erc20ChainIDs) + if err != nil { + return nil, err + } + + erc721Balances := make(CollectiblesByChain) + if len(erc721ChainIDs) > 0 { + balances, err := m.GetOwnedERC721Tokens(accounts, erc721TokenCriteriaByChain, erc721ChainIDs) + if err != nil { + return nil, err + } + + erc721Balances = balances + } + + return calculatePermissionedBalances(allChainIDs, accountAddresses, erc20Balances, erc721Balances, tokenPermissions), nil +} diff --git a/protocol/communities/permissioned_balances_test.go b/protocol/communities/permissioned_balances_test.go new file mode 100644 index 000000000..b2f62de7a --- /dev/null +++ b/protocol/communities/permissioned_balances_test.go @@ -0,0 +1,319 @@ +package communities + +import ( + "context" + "math/big" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/status-im/status-go/protocol/requests" + "github.com/status-im/status-go/services/wallet/bigint" + "github.com/status-im/status-go/services/wallet/thirdparty" + + _ "github.com/mutecomm/go-sqlcipher/v4" // require go-sqlcipher that overrides default implementation + + "github.com/status-im/status-go/protocol/protobuf" +) + +func (s *ManagerSuite) Test_calculatePermissionedBalances() { + var mainnetID uint64 = 1 + var arbitrumID uint64 = 42161 + var gnosisID uint64 = 100 + chainIDs := []uint64{mainnetID, arbitrumID} + + mainnetSNTContractAddress := gethcommon.HexToAddress("0xC") + mainnetETHContractAddress := gethcommon.HexToAddress("0xA") + arbitrumETHContractAddress := gethcommon.HexToAddress("0xB") + mainnetTMasterAddress := gethcommon.HexToAddress("0x123") + mainnetOwnerAddress := gethcommon.HexToAddress("0x1234") + + account1Address := gethcommon.HexToAddress("0x1") + account2Address := gethcommon.HexToAddress("0x2") + account3Address := gethcommon.HexToAddress("0x3") + accountAddresses := []gethcommon.Address{account1Address, account2Address, account3Address} + + erc20Balances := make(BalancesByChain) + erc721Balances := make(CollectiblesByChain) + + erc20Balances[mainnetID] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) + erc20Balances[arbitrumID] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) + erc20Balances[gnosisID] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) + erc721Balances[mainnetID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress) + + // Account 1 balances + erc20Balances[mainnetID][account1Address] = make(map[gethcommon.Address]*hexutil.Big) + erc20Balances[mainnetID][account1Address][mainnetETHContractAddress] = intToBig(10) + erc20Balances[arbitrumID][account1Address] = make(map[gethcommon.Address]*hexutil.Big) + erc20Balances[arbitrumID][account1Address][arbitrumETHContractAddress] = intToBig(25) + + // Account 2 balances + erc20Balances[mainnetID][account2Address] = make(map[gethcommon.Address]*hexutil.Big) + erc20Balances[mainnetID][account2Address][mainnetSNTContractAddress] = intToBig(120) + erc721Balances[mainnetID][account2Address] = make(thirdparty.TokenBalancesPerContractAddress) + erc721Balances[mainnetID][account2Address][mainnetTMasterAddress] = []thirdparty.TokenBalance{ + thirdparty.TokenBalance{ + TokenID: uintToDecBig(456), + Balance: uintToDecBig(1), + }, + thirdparty.TokenBalance{ + TokenID: uintToDecBig(123), + Balance: uintToDecBig(2), + }, + } + erc721Balances[mainnetID][account2Address][mainnetOwnerAddress] = []thirdparty.TokenBalance{ + thirdparty.TokenBalance{ + TokenID: uintToDecBig(100), + Balance: uintToDecBig(6), + }, + thirdparty.TokenBalance{ + TokenID: uintToDecBig(101), + Balance: uintToDecBig(1), + }, + } + + erc20Balances[arbitrumID][account2Address] = make(map[gethcommon.Address]*hexutil.Big) + erc20Balances[arbitrumID][account2Address][arbitrumETHContractAddress] = intToBig(2) + + // Account 3 balances. This account is used to assert zeroed balances are + // removed from the final response. + erc20Balances[mainnetID][account3Address] = make(map[gethcommon.Address]*hexutil.Big) + erc20Balances[mainnetID][account3Address][mainnetETHContractAddress] = intToBig(0) + + // A balance that should be ignored because the list of wallet addresses don't + // contain any wallet in the Gnosis chain. + erc20Balances[gnosisID][gethcommon.HexToAddress("0xF")] = make(map[gethcommon.Address]*hexutil.Big) + erc20Balances[gnosisID][gethcommon.HexToAddress("0xF")][gethcommon.HexToAddress("0x99")] = intToBig(5) + + tokenPermissions := []*CommunityTokenPermission{ + &CommunityTokenPermission{ + CommunityTokenPermission: &protobuf.CommunityTokenPermission{ + Type: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC721, + Symbol: "TMTEST", + Name: "TMaster-Test", + Amount: "1", + TokenIds: []uint64{123, 456}, + ContractAddresses: map[uint64]string{mainnetID: mainnetTMasterAddress.Hex()}, + }, + }, + }, + }, + &CommunityTokenPermission{ + CommunityTokenPermission: &protobuf.CommunityTokenPermission{ + Type: protobuf.CommunityTokenPermission_BECOME_TOKEN_OWNER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC721, + Symbol: "OWNTEST", + Name: "Owner-Test", + Amount: "5", + // No account has a positive balance for these token IDs, so we + // expect this collectible to not be present in the final result. + TokenIds: []uint64{666}, + ContractAddresses: map[uint64]string{mainnetID: mainnetOwnerAddress.Hex()}, + }, + }, + }, + }, + &CommunityTokenPermission{ + CommunityTokenPermission: &protobuf.CommunityTokenPermission{ + Type: protobuf.CommunityTokenPermission_BECOME_ADMIN, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "ETH", + Name: "Ethereum", + Amount: "20", + Decimals: 18, + ContractAddresses: map[uint64]string{ + arbitrumID: arbitrumETHContractAddress.Hex(), + mainnetID: mainnetETHContractAddress.Hex(), + }, + }, + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "ETH", + Name: "Ethereum", + Amount: "4", + Decimals: 18, + ContractAddresses: map[uint64]string{arbitrumID: arbitrumETHContractAddress.Hex()}, + }, + }, + }, + }, + &CommunityTokenPermission{ + CommunityTokenPermission: &protobuf.CommunityTokenPermission{ + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "SNT", + Name: "Status", + Amount: "1000", + Decimals: 16, + ContractAddresses: map[uint64]string{mainnetID: mainnetSNTContractAddress.Hex()}, + }, + }, + }, + }, + &CommunityTokenPermission{ + CommunityTokenPermission: &protobuf.CommunityTokenPermission{ + // Unknown permission should be ignored. + Type: protobuf.CommunityTokenPermission_UNKNOWN_TOKEN_PERMISSION, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "DAI", + Name: "Dai", + Amount: "7", + Decimals: 12, + ContractAddresses: map[uint64]string{mainnetID: "0x1234567"}, + }, + }, + }, + }, + } + + actual := calculatePermissionedBalances( + chainIDs, + accountAddresses, + erc20Balances, + erc721Balances, + tokenPermissions, + ) + + expected := make(map[gethcommon.Address][]PermissionedBalance) + expected[account1Address] = []PermissionedBalance{ + PermissionedBalance{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "ETH", + Name: "Ethereum", + Decimals: 18, + Amount: &bigint.BigInt{Int: big.NewInt(35)}, + }, + } + expected[account2Address] = []PermissionedBalance{ + PermissionedBalance{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "ETH", + Name: "Ethereum", + Decimals: 18, + Amount: &bigint.BigInt{Int: big.NewInt(2)}, + }, + PermissionedBalance{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "SNT", + Name: "Status", + Decimals: 16, + Amount: &bigint.BigInt{Int: big.NewInt(120)}, + }, + PermissionedBalance{ + Type: protobuf.CommunityTokenType_ERC721, + Symbol: "TMTEST", + Name: "TMaster-Test", + Amount: &bigint.BigInt{Int: big.NewInt(1)}, + }, + } + + _, ok := actual[account1Address] + s.Require().True(ok, "not found account1Address='%s'", account1Address) + _, ok = actual[account1Address] + s.Require().True(ok, "not found account2Address='%s'", account2Address) + + for accountAddress, permissionedTokens := range actual { + s.Require().ElementsMatch(expected[accountAddress], permissionedTokens, "accountAddress='%s'", accountAddress) + } +} + +func (s *ManagerSuite) Test_GetPermissionedBalances() { + m, collectiblesManager, tokenManager := s.setupManagerForTokenPermissions() + s.Require().NotNil(m) + s.Require().NotNil(collectiblesManager) + + request := &requests.CreateCommunity{ + Membership: protobuf.CommunityPermissions_AUTO_ACCEPT, + } + community, err := m.CreateCommunity(request, true) + s.Require().NoError(err) + s.Require().NotNil(community) + + accountAddress := gethcommon.HexToAddress("0x1") + accountAddresses := []gethcommon.Address{accountAddress} + + var chainID uint64 = 5 + erc20ETHAddress := gethcommon.HexToAddress("0xA") + erc721Address := gethcommon.HexToAddress("0x123") + + permissionRequest := &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "ETH", + Name: "Ethereum", + Amount: "3", + Decimals: 18, + ContractAddresses: map[uint64]string{chainID: erc20ETHAddress.Hex()}, + }, + }, + } + _, changes, err := m.CreateCommunityTokenPermission(permissionRequest) + s.Require().NoError(err) + s.Require().Len(changes.TokenPermissionsAdded, 1) + + permissionRequest = &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_BECOME_TOKEN_MASTER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC721, + Symbol: "TMTEST", + Name: "TMaster-Test", + Amount: "1", + TokenIds: []uint64{666}, + ContractAddresses: map[uint64]string{chainID: erc721Address.Hex()}, + }, + }, + } + _, changes, err = m.CreateCommunityTokenPermission(permissionRequest) + s.Require().NoError(err) + s.Require().Len(changes.TokenPermissionsAdded, 1) + + tokenManager.setResponse(chainID, accountAddress, erc20ETHAddress, 42) + collectiblesManager.setResponse(chainID, accountAddress, erc721Address, []thirdparty.TokenBalance{ + thirdparty.TokenBalance{ + TokenID: uintToDecBig(666), + Balance: uintToDecBig(15), + }, + }) + + actual, err := m.GetPermissionedBalances(context.Background(), community.ID(), accountAddresses) + s.Require().NoError(err) + + expected := make(map[gethcommon.Address][]PermissionedBalance) + expected[accountAddress] = []PermissionedBalance{ + PermissionedBalance{ + Type: protobuf.CommunityTokenType_ERC20, + Symbol: "ETH", + Name: "Ethereum", + Decimals: 18, + Amount: &bigint.BigInt{Int: big.NewInt(42)}, + }, + PermissionedBalance{ + Type: protobuf.CommunityTokenType_ERC721, + Symbol: "TMTEST", + Name: "TMaster-Test", + Amount: &bigint.BigInt{Int: big.NewInt(1)}, + }, + } + + _, ok := actual[accountAddress] + s.Require().True(ok, "not found accountAddress='%s'", accountAddress) + + for address, permissionedBalances := range actual { + s.Require().ElementsMatch(expected[address], permissionedBalances) + } +}