diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 676ebdf99..d3fa0948c 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -27,7 +27,6 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/event" "github.com/status-im/status-go/account" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" @@ -41,7 +40,7 @@ import ( "github.com/status-im/status-go/protocol/transport" "github.com/status-im/status-go/services/wallet/bigint" walletcommon "github.com/status-im/status-go/services/wallet/common" - "github.com/status-im/status-go/services/wallet/thirdparty/opensea" + "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/signal" ) @@ -71,11 +70,11 @@ type Manager struct { identity *ecdsa.PrivateKey accountsManager account.Manager tokenManager TokenManager + collectiblesManager CollectiblesManager logger *zap.Logger stdoutLogger *zap.Logger transport *transport.Transport quit chan struct{} - openseaClientBuilder openseaClientBuilder torrentConfig *params.TorrentConfig torrentClient *torrent.Client walletConfig *params.WalletConfig @@ -87,21 +86,6 @@ type Manager struct { stopped bool } -type openseaClient interface { - FetchAllAssetsByOwnerAndContractAddress(owner gethcommon.Address, contractAddresses []gethcommon.Address, cursor string, limit int) (*opensea.AssetContainer, error) -} - -type openseaClientBuilder interface { - NewOpenseaClient(uint64, string, *event.Feed) (openseaClient, error) -} - -type defaultOpenseaBuilder struct { -} - -func (b *defaultOpenseaBuilder) NewOpenseaClient(chainID uint64, apiKey string, feed *event.Feed) (openseaClient, error) { - return opensea.NewOpenseaClient(chainID, apiKey, nil) -} - type HistoryArchiveDownloadTask struct { CancelChan chan struct{} Waiter sync.WaitGroup @@ -123,10 +107,10 @@ func (t *HistoryArchiveDownloadTask) Cancel() { } type managerOptions struct { - accountsManager account.Manager - tokenManager TokenManager - walletConfig *params.WalletConfig - openseaClientBuilder openseaClientBuilder + accountsManager account.Manager + tokenManager TokenManager + collectiblesManager CollectiblesManager + walletConfig *params.WalletConfig } type TokenManager interface { @@ -157,6 +141,10 @@ func (m *DefaultTokenManager) GetAllChainIDs() ([]uint64, error) { return chainIDs, nil } +type CollectiblesManager interface { + FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) +} + func (m *DefaultTokenManager) GetBalancesByChain(ctx context.Context, accounts, tokenAddresses []gethcommon.Address, chainIDs []uint64) (BalancesByChain, error) { clients, err := m.tokenManager.RPCClient.EthClients(chainIDs) if err != nil { @@ -175,9 +163,9 @@ func WithAccountManager(accountsManager account.Manager) ManagerOption { } } -func WithOpenseaClientBuilder(builder openseaClientBuilder) ManagerOption { +func WithCollectiblesManager(collectiblesManager CollectiblesManager) ManagerOption { return func(opts *managerOptions) { - opts.openseaClientBuilder = builder + opts.collectiblesManager = collectiblesManager } } @@ -235,6 +223,10 @@ func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Pr manager.accountsManager = managerConfig.accountsManager } + if managerConfig.collectiblesManager != nil { + manager.collectiblesManager = managerConfig.collectiblesManager + } + if managerConfig.tokenManager != nil { manager.tokenManager = managerConfig.tokenManager } @@ -243,12 +235,6 @@ func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Pr manager.walletConfig = managerConfig.walletConfig } - if managerConfig.openseaClientBuilder != nil { - manager.openseaClientBuilder = managerConfig.openseaClientBuilder - } else { - manager.openseaClientBuilder = &defaultOpenseaBuilder{} - } - if verifier != nil { sub := verifier.Subscribe() @@ -2002,8 +1988,9 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss continue } - if _, exists := ownedERC721Tokens[chainID][account][strings.ToLower(address)]; exists { - + tokenBalances := ownedERC721Tokens[chainID][account][gethcommon.HexToAddress(address)] + if len(tokenBalances) > 0 { + // 'account' owns some TokenID owned from contract 'address' if _, exists := accountsChainIDsCombinations[account]; !exists { accountsChainIDsCombinations[account] = make(map[uint64]bool) } @@ -2019,8 +2006,8 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss for _, tokenID := range tokenRequirement.TokenIds { tokenIDBigInt := new(big.Int).SetUint64(tokenID) - for _, asset := range ownedERC721Tokens[chainID][account][strings.ToLower(address)] { - if asset.TokenID.Cmp(tokenIDBigInt) == 0 { + for _, asset := range tokenBalances { + if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 { tokenRequirementMet = true accountsChainIDsCombinations[account][chainID] = true break tokenIDsLoop @@ -2143,15 +2130,14 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss return response, nil } -type CollectiblesByChain = map[uint64]map[gethcommon.Address]map[string][]opensea.Asset +type CollectiblesByChain = map[uint64]map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) { - - if m.walletConfig == nil || m.walletConfig.OpenseaAPIKey == "" { - return nil, errors.New("no opensea client") + if m.collectiblesManager == nil { + return nil, errors.New("no collectibles manager") } - ownedERC721Tokens := make(map[uint64]map[gethcommon.Address]map[string][]opensea.Asset) + ownedERC721Tokens := make(CollectiblesByChain) for chainID, erc721Tokens := range tokenRequirements { @@ -2166,41 +2152,22 @@ func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tok continue } - client, err := m.openseaClientBuilder.NewOpenseaClient(chainID, m.walletConfig.OpenseaAPIKey, nil) - if err != nil { - return nil, err - } - 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]map[string][]opensea.Asset) + ownedERC721Tokens[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress) } for _, owner := range walletAddresses { - assets, err := client.FetchAllAssetsByOwnerAndContractAddress(owner, contractAddresses, "", 5) + balances, err := m.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, owner, contractAddresses) if err != nil { m.logger.Info("couldn't fetch owner assets", zap.Error(err)) return nil, err } - - if len(assets.Assets) == 0 { - continue - } - - if _, exists := ownedERC721Tokens[chainID][owner]; !exists { - ownedERC721Tokens[chainID][owner] = make(map[string][]opensea.Asset, 0) - } - - for _, asset := range assets.Assets { - if _, exists := ownedERC721Tokens[chainID][owner][asset.Contract.Address]; !exists { - ownedERC721Tokens[chainID][owner][asset.Contract.Address] = make([]opensea.Asset, 0) - } - ownedERC721Tokens[chainID][owner][asset.Contract.Address] = append(ownedERC721Tokens[chainID][owner][asset.Contract.Address], asset) - } + ownedERC721Tokens[chainID][owner] = balances } } return ownedERC721Tokens, nil diff --git a/protocol/communities/manager_test.go b/protocol/communities/manager_test.go index 1e5ae7c50..886c42ed5 100644 --- a/protocol/communities/manager_test.go +++ b/protocol/communities/manager_test.go @@ -14,14 +14,14 @@ import ( gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/event" "github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/eth-node/types" userimages "github.com/status-im/status-go/images" "github.com/status-im/status-go/params" "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/transport" - "github.com/status-im/status-go/services/wallet/thirdparty/opensea" + "github.com/status-im/status-go/services/wallet/bigint" + "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/golang/protobuf/proto" _ "github.com/mutecomm/go-sqlcipher/v4" // require go-sqlcipher that overrides default implementation @@ -63,6 +63,17 @@ func intToBig(n int64) *hexutil.Big { return (*hexutil.Big)(big.NewInt(n)) } +func uintToDecBig(n uint64) *bigint.BigInt { + return &bigint.BigInt{Int: big.NewInt(int64(n))} +} + +func tokenBalance(tokenID uint64, balance uint64) thirdparty.TokenBalance { + return thirdparty.TokenBalance{ + TokenID: uintToDecBig(tokenID), + Balance: uintToDecBig(balance), + } +} + func (s *ManagerSuite) getHistoryTasksCount() int { // sync.Map doesn't have a Len function, so we need to count manually count := 0 @@ -73,11 +84,26 @@ func (s *ManagerSuite) getHistoryTasksCount() int { return count } -type openseaClientTestBuilder struct { +type testCollectiblesManager struct { + response map[uint64]map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress } -func (b *openseaClientTestBuilder) NewOpenseaClient(chainID uint64, apiKey string, feed *event.Feed) (openseaClient, error) { - return opensea.NewOpenseaClient(chainID, apiKey, nil) +func (m *testCollectiblesManager) setResponse(chainID uint64, walletAddress gethcommon.Address, contractAddress gethcommon.Address, balances []thirdparty.TokenBalance) { + if m.response == nil { + m.response = make(map[uint64]map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress) + } + if m.response[chainID] == nil { + m.response[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress) + } + if m.response[chainID][walletAddress] == nil { + m.response[chainID][walletAddress] = make(thirdparty.TokenBalancesPerContractAddress) + } + + m.response[chainID][walletAddress][contractAddress] = balances +} + +func (m *testCollectiblesManager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) { + return m.response[chainID][ownerAddress], nil } type testTokenManager struct { @@ -110,7 +136,7 @@ func (m *testTokenManager) GetBalancesByChain(ctx context.Context, accounts, tok return m.response, nil } -func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenManager) { +func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testCollectiblesManager, *testTokenManager) { db, err := appdatabase.InitializeDB(sqlite.InMemoryPath, "", sqlite.ReducedKDFIterationsNumber) s.NoError(err, "creating sqlite db instance") err = sqlite.Migrate(db) @@ -120,13 +146,14 @@ func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenMa s.Require().NoError(err) s.Require().NoError(err) + cm := &testCollectiblesManager{} tm := &testTokenManager{} options := []ManagerOption{ WithWalletConfig(¶ms.WalletConfig{ OpenseaAPIKey: "some-key", }), - WithOpenseaClientBuilder(&openseaClientTestBuilder{}), + WithCollectiblesManager(cm), WithTokenManager(tm), } @@ -134,11 +161,11 @@ func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenMa s.Require().NoError(err) s.Require().NoError(m.Start()) - return m, tm + return m, cm, tm } func (s *ManagerSuite) TestRetrieveTokens() { - m, tm := s.setupManagerForTokenPermissions() + m, _, tm := s.setupManagerForTokenPermissions() var chainID uint64 = 5 contractAddresses := make(map[uint64]string) @@ -185,6 +212,56 @@ func (s *ManagerSuite) TestRetrieveTokens() { s.Require().False(resp.Satisfied) } +func (s *ManagerSuite) TestRetrieveCollectibles() { + m, cm, _ := s.setupManagerForTokenPermissions() + + var chainID uint64 = 5 + contractAddresses := make(map[uint64]string) + contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a" + + tokenID := uint64(10) + var tokenBalances []thirdparty.TokenBalance + + var tokenCriteria = []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + ContractAddresses: contractAddresses, + TokenIds: []uint64{tokenID}, + Type: protobuf.CommunityTokenType_ERC721, + }, + } + + var permissions = []*protobuf.CommunityTokenPermission{ + &protobuf.CommunityTokenPermission{ + Id: "some-id", + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: tokenCriteria, + }, + } + + accountChainIDsCombination := []*AccountChainIDsCombination{ + &AccountChainIDsCombination{ + Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + ChainIDs: []uint64{chainID}, + }, + } + + // Set response to exactly the right one + tokenBalances = []thirdparty.TokenBalance{tokenBalance(tokenID, 1)} + cm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), tokenBalances) + resp, err := m.checkPermissionToJoin(permissions, accountChainIDsCombination, false) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().True(resp.Satisfied) + + // Set balances to 0 + tokenBalances = []thirdparty.TokenBalance{} + cm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), tokenBalances) + resp, err = m.checkPermissionToJoin(permissions, accountChainIDsCombination, false) + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().False(resp.Satisfied) +} + func (s *ManagerSuite) TestCreateCommunity() { request := &requests.CreateCommunity{ @@ -812,7 +889,7 @@ func (s *ManagerSuite) TestUnseedHistoryArchiveTorrent() { func (s *ManagerSuite) TestCheckChannelPermissions_NoPermissions() { - m, tm := s.setupManagerForTokenPermissions() + m, _, tm := s.setupManagerForTokenPermissions() var chainID uint64 = 5 contractAddresses := make(map[uint64]string) @@ -841,7 +918,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_NoPermissions() { func (s *ManagerSuite) TestCheckChannelPermissions_ViewOnlyPermissions() { - m, tm := s.setupManagerForTokenPermissions() + m, _, tm := s.setupManagerForTokenPermissions() var chainID uint64 = 5 contractAddresses := make(map[uint64]string) @@ -899,7 +976,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewOnlyPermissions() { func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissions() { - m, tm := s.setupManagerForTokenPermissions() + m, _, tm := s.setupManagerForTokenPermissions() var chainID uint64 = 5 contractAddresses := make(map[uint64]string) @@ -958,7 +1035,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissions() { func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombination() { - m, tm := s.setupManagerForTokenPermissions() + m, _, tm := s.setupManagerForTokenPermissions() var chainID uint64 = 5 contractAddresses := make(map[uint64]string) @@ -1032,7 +1109,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombina func (s *ManagerSuite) TestCheckAllChannelsPermissions_EmptyPermissions() { - m, _ := s.setupManagerForTokenPermissions() + m, _, _ := s.setupManagerForTokenPermissions() createRequest := &requests.CreateCommunity{ Name: "channel permission community", @@ -1079,7 +1156,7 @@ func (s *ManagerSuite) TestCheckAllChannelsPermissions_EmptyPermissions() { func (s *ManagerSuite) TestCheckAllChannelsPermissions() { - m, tm := s.setupManagerForTokenPermissions() + m, _, tm := s.setupManagerForTokenPermissions() var chatID1 string var chatID2 string diff --git a/protocol/messenger.go b/protocol/messenger.go index 76868a945..9fc8fb258 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -422,10 +422,19 @@ func NewMessenger( ensVerifier := ens.New(node, logger, transp, database, c.verifyENSURL, c.verifyENSContractAddress) + var walletAPI *wallet.API + if c.walletService != nil { + walletAPI = wallet.NewAPI(c.walletService) + } + managerOptions := []communities.ManagerOption{ communities.WithAccountManager(accountsManager), } + if walletAPI != nil { + managerOptions = append(managerOptions, communities.WithCollectiblesManager(walletAPI)) + } + if c.tokenManager != nil { managerOptions = append(managerOptions, communities.WithTokenManager(c.tokenManager)) } else if c.rpcClient != nil { @@ -545,7 +554,7 @@ func NewMessenger( messenger.mentionsManager = NewMentionManager(messenger) if c.walletService != nil { - messenger.walletAPI = wallet.NewAPI(c.walletService) + messenger.walletAPI = walletAPI } if c.outputMessagesCSV { diff --git a/services/wallet/api.go b/services/wallet/api.go index 8e6f5b5f6..9c6db3d86 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -338,6 +338,11 @@ func (api *API) GetCollectibleOwnersByContractAddress(chainID uint64, contractAd return api.s.collectiblesManager.FetchNFTOwnersByContractAddress(chainID, contractAddress) } +func (api *API) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) { + log.Debug("call to FetchBalancesByOwnerAndContractAddress") + return api.s.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses) +} + func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error { log.Debug("call to AddEthereumChain") return api.s.rpcClient.NetworkManager.Upsert(&network) diff --git a/services/wallet/collectibles/collectibles.go b/services/wallet/collectibles/collectibles.go index 6f23d1f2c..8c0e03808 100644 --- a/services/wallet/collectibles/collectibles.go +++ b/services/wallet/collectibles/collectibles.go @@ -3,6 +3,7 @@ package collectibles import ( "context" "fmt" + "math/big" "strings" "sync" "time" @@ -14,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/event" "github.com/status-im/status-go/contracts/collectibles" "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/services/wallet/thirdparty/opensea" ) @@ -121,6 +123,48 @@ func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner commo return assetContainer, nil } +// Need to combine different providers to support all needed ChainIDs +func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) { + ret := make(thirdparty.TokenBalancesPerContractAddress) + + for _, contractAddress := range contractAddresses { + ret[contractAddress] = make([]thirdparty.TokenBalance, 0) + } + + // Try with more direct endpoint first (OpenSea) + assetsContainer, err := o.FetchAllAssetsByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses, "", 0) + if err == opensea.ErrChainIDNotSupported { + // Use contract ownership providers + for _, contractAddress := range contractAddresses { + ownership, err := o.FetchNFTOwnersByContractAddress(chainID, contractAddress) + if err != nil { + return nil, err + } + for _, nftOwner := range ownership.Owners { + if nftOwner.OwnerAddress == ownerAddress { + ret[contractAddress] = nftOwner.TokenBalances + break + } + } + } + } else if err == nil { + // OpenSea could provide + for _, asset := range assetsContainer.Assets { + contractAddress := common.HexToAddress(asset.Contract.Address) + balance := thirdparty.TokenBalance{ + TokenID: asset.TokenID, + Balance: &bigint.BigInt{Int: big.NewInt(1)}, + } + ret[contractAddress] = append(ret[contractAddress], balance) + } + } else { + // OpenSea could have provided, but returned error + return nil, err + } + + return ret, nil +} + func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*opensea.AssetContainer, error) { client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey, o.walletFeed) if err != nil { diff --git a/services/wallet/thirdparty/types.go b/services/wallet/thirdparty/types.go index fec7e2af3..ae687ee99 100644 --- a/services/wallet/thirdparty/types.go +++ b/services/wallet/thirdparty/types.go @@ -67,6 +67,8 @@ type TokenBalance struct { Balance *bigint.BigInt `json:"balance"` } +type TokenBalancesPerContractAddress = map[common.Address][]TokenBalance + type NFTOwner struct { OwnerAddress common.Address `json:"ownerAddress"` TokenBalances []TokenBalance `json:"tokenBalances"`