diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index a981701cb..55d30827b 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -143,7 +143,7 @@ func (m *DefaultTokenManager) GetAllChainIDs() ([]uint64, error) { } type CollectiblesManager interface { - FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) + FetchBalancesByOwnerAndContractAddress(chainID walletcommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) } func (m *DefaultTokenManager) GetBalancesByChain(ctx context.Context, accounts, tokenAddresses []gethcommon.Address, chainIDs []uint64) (BalancesByChain, error) { @@ -2028,7 +2028,8 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss } chainIDLoopERC721: - for chainID, address := range tokenRequirement.ContractAddresses { + for chainID, addressStr := range tokenRequirement.ContractAddresses { + contractAddress := gethcommon.HexToAddress(addressStr) if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 { continue chainIDLoopERC721 } @@ -2038,7 +2039,7 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss continue } - tokenBalances := ownedERC721Tokens[chainID][account][gethcommon.HexToAddress(address)] + tokenBalances := ownedERC721Tokens[chainID][account][contractAddress] if len(tokenBalances) > 0 { // 'account' owns some TokenID owned from contract 'address' if _, exists := accountsChainIDsCombinations[account]; !exists { @@ -2189,8 +2190,6 @@ func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tok ownedERC721Tokens := make(CollectiblesByChain) - client := m.openseaClientBuilder.NewOpenseaClient(m.walletConfig.OpenseaAPIKey, nil) - for chainID, erc721Tokens := range tokenRequirements { skipChain := true @@ -2214,7 +2213,7 @@ func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tok } for _, owner := range walletAddresses { - balances, err := m.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, owner, contractAddresses) + balances, err := m.collectiblesManager.FetchBalancesByOwnerAndContractAddress(walletcommon.ChainID(chainID), owner, contractAddresses) if err != nil { m.logger.Info("couldn't fetch owner assets", zap.Error(err)) return nil, err diff --git a/protocol/communities/manager_test.go b/protocol/communities/manager_test.go index 886c42ed5..3b79856f4 100644 --- a/protocol/communities/manager_test.go +++ b/protocol/communities/manager_test.go @@ -21,6 +21,7 @@ import ( "github.com/status-im/status-go/protocol/requests" "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" "github.com/golang/protobuf/proto" @@ -102,8 +103,8 @@ func (m *testCollectiblesManager) setResponse(chainID uint64, walletAddress geth 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 +func (m *testCollectiblesManager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) { + return m.response[uint64(chainID)][ownerAddress], nil } type testTokenManager struct { diff --git a/services/ext/service.go b/services/ext/service.go index 274b65d5d..daac64703 100644 --- a/services/ext/service.go +++ b/services/ext/service.go @@ -545,12 +545,12 @@ func tokenURIToCommunityID(tokenURI string) string { return communityID } -func (s *Service) CanProvideCollectibleMetadata(chainID uint64, id thirdparty.CollectibleUniqueID, tokenURI string) (bool, error) { +func (s *Service) CanProvideCollectibleMetadata(id thirdparty.CollectibleUniqueID, tokenURI string) (bool, error) { ret := tokenURI != "" && tokenURIToCommunityID(tokenURI) != "" return ret, nil } -func (s *Service) FetchCollectibleMetadata(chainID uint64, id thirdparty.CollectibleUniqueID, tokenURI string) (*thirdparty.CollectibleMetadata, error) { +func (s *Service) FetchCollectibleMetadata(id thirdparty.CollectibleUniqueID, tokenURI string) (*thirdparty.CollectibleData, error) { if s.messenger == nil { return nil, fmt.Errorf("messenger not ready") } @@ -573,12 +573,16 @@ func (s *Service) FetchCollectibleMetadata(chainID uint64, id thirdparty.Collect for _, tokenMetadata := range tokensMetadata { contractAddresses := tokenMetadata.GetContractAddresses() - if contractAddresses[chainID] == id.ContractAddress.Hex() { - return &thirdparty.CollectibleMetadata{ - Name: tokenMetadata.GetName(), - Description: tokenMetadata.GetDescription(), - CollectionImageURL: tokenMetadata.GetImage(), - ImageURL: tokenMetadata.GetImage(), + if contractAddresses[uint64(id.ChainID)] == id.ContractAddress.Hex() { + return &thirdparty.CollectibleData{ + ID: id, + Name: tokenMetadata.GetName(), + Description: tokenMetadata.GetDescription(), + ImageURL: tokenMetadata.GetImage(), + CollectionData: thirdparty.CollectionData{ + Name: tokenMetadata.GetName(), + ImageURL: tokenMetadata.GetImage(), + }, }, nil } } diff --git a/services/wallet/api.go b/services/wallet/api.go index 502be978f..c9485fe0a 100644 --- a/services/wallet/api.go +++ b/services/wallet/api.go @@ -299,13 +299,27 @@ func (api *API) GetCryptoOnRamps(ctx context.Context) ([]CryptoOnRamp, error) { return api.s.cryptoOnRampManager.Get() } -func (api *API) GetOpenseaCollectionsByOwner(ctx context.Context, chainID uint64, owner common.Address) ([]opensea.OwnedCollection, error) { - log.Debug("call to get opensea collections") +/* + Collectibles API Start +*/ + +func (api *API) FetchBalancesByOwnerAndContractAddress(chainID wcommon.ChainID, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) { + log.Debug("call to FetchBalancesByOwnerAndContractAddress") + return api.s.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses) +} + +// Old Collectibles API - To be deprecated +func (api *API) GetOpenseaCollectionsByOwner(ctx context.Context, chainID wcommon.ChainID, owner common.Address) ([]opensea.OwnedCollection, error) { + log.Debug("call to GetOpenseaCollectionsByOwner") return api.s.collectiblesManager.FetchAllCollectionsByOwner(chainID, owner) } -// Kept for compatibility with mobile app -func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainID uint64, owner common.Address, collectionSlug string, limit int) ([]opensea.Asset, error) { +func (api *API) GetOpenseaAssetsByOwnerAndCollectionWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) { + log.Debug("call to GetOpenseaAssetsByOwnerAndCollectionWithCursor") + return api.s.collectiblesManager.FetchAllOpenseaAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit) +} + +func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainID wcommon.ChainID, owner common.Address, collectionSlug string, limit int) ([]opensea.Asset, error) { container, err := api.GetOpenseaAssetsByOwnerAndCollectionWithCursor(ctx, chainID, owner, collectionSlug, "", limit) if err != nil { return nil, err @@ -313,35 +327,34 @@ func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainI return container.Assets, nil } -func (api *API) GetOpenseaAssetsByOwnerAndCollectionWithCursor(ctx context.Context, chainID uint64, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) { - log.Debug("call to get opensea assets") +func (api *API) GetCollectiblesByOwnerAndCollectionWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { + log.Debug("call to GetCollectiblesByOwnerAndCollectionWithCursor") return api.s.collectiblesManager.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit) } -func (api *API) GetOpenseaAssetsByOwnerWithCursor(ctx context.Context, chainID uint64, owner common.Address, cursor string, limit int) (*opensea.AssetContainer, error) { - log.Debug("call to FetchAllAssetsByOwner") +func (api *API) GetCollectiblesByOwnerWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { + log.Debug("call to GetCollectiblesByOwnerWithCursor") return api.s.collectiblesManager.FetchAllAssetsByOwner(chainID, owner, cursor, limit) } -func (api *API) GetOpenseaAssetsByOwnerAndContractAddressWithCursor(ctx context.Context, chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*opensea.AssetContainer, error) { - log.Debug("call to GetOpenseaAssetsByOwnerAndContractAddressWithCursor") +func (api *API) GetCollectiblesByOwnerAndContractAddressWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { + log.Debug("call to GetCollectiblesByOwnerAndContractAddressWithCursor") return api.s.collectiblesManager.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit) } -func (api *API) GetOpenseaAssetsByNFTUniqueID(ctx context.Context, chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID, limit int) (*opensea.AssetContainer, error) { - log.Debug("call to GetOpenseaAssetsByNFTUniqueID") - return api.s.collectiblesManager.FetchAssetsByNFTUniqueID(chainID, uniqueIDs, limit) +func (api *API) GetCollectiblesByUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) { + log.Debug("call to GetCollectiblesByUniqueID") + return api.s.collectiblesManager.FetchAssetsByCollectibleUniqueID(uniqueIDs) } -func (api *API) GetCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { +func (api *API) GetCollectibleOwnersByContractAddress(chainID wcommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { log.Debug("call to GetCollectibleOwnersByContractAddress") return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(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) -} +/* + Collectibles API End +*/ func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error { log.Debug("call to AddEthereumChain") diff --git a/services/wallet/collectibles/collectibles.go b/services/wallet/collectibles/collectibles.go index d092fd6c7..98655787a 100644 --- a/services/wallet/collectibles/collectibles.go +++ b/services/wallet/collectibles/collectibles.go @@ -12,10 +12,10 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "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" + walletCommon "github.com/status-im/status-go/services/wallet/common" "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/services/wallet/thirdparty/opensea" ) @@ -24,8 +24,6 @@ const requestTimeout = 5 * time.Second const hystrixContractOwnershipClientName = "contractOwnershipClient" -const maxNFTDescriptionLength = 1024 - // ERC721 does not support function "TokenURI" if call // returns error starting with one of these strings var noTokenURIErrorPrefixes = []string{ @@ -39,11 +37,11 @@ type Manager struct { fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider metadataProvider thirdparty.CollectibleMetadataProvider opensea *opensea.Client - nftCache map[uint64]map[string]opensea.Asset + nftCache map[walletCommon.ChainID]map[string]thirdparty.CollectibleData nftCacheLock sync.RWMutex } -func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, openseaAPIKey string, walletFeed *event.Feed) *Manager { +func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, opensea *opensea.Client) *Manager { hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{ Timeout: 10000, MaxConcurrentRequests: 100, @@ -55,8 +53,7 @@ func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty. rpcClient: rpcClient, mainContractOwnershipProvider: mainContractOwnershipProvider, fallbackContractOwnershipProvider: fallbackContractOwnershipProvider, - opensea: opensea.NewClient(openseaAPIKey, walletFeed), - nftCache: make(map[uint64]map[string]opensea.Asset), + opensea: opensea, } } @@ -94,17 +91,21 @@ func (o *Manager) SetMetadataProvider(metadataProvider thirdparty.CollectibleMet o.metadataProvider = metadataProvider } -func (o *Manager) FetchAllCollectionsByOwner(chainID uint64, owner common.Address) ([]opensea.OwnedCollection, error) { +func (o *Manager) FetchAllCollectionsByOwner(chainID walletCommon.ChainID, owner common.Address) ([]opensea.OwnedCollection, error) { return o.opensea.FetchAllCollectionsByOwner(chainID, owner) } -func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) { +func (o *Manager) FetchAllOpenseaAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) { + return o.opensea.FetchAllOpenseaAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit) +} + +func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit) if err != nil { return nil, err } - err = o.processAssets(chainID, assetContainer.Assets) + err = o.processAssets(assetContainer.Collectibles) if err != nil { return nil, err } @@ -113,7 +114,7 @@ func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner commo } // 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) { +func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.ChainID, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) { ret := make(thirdparty.TokenBalancesPerContractAddress) for _, contractAddress := range contractAddresses { @@ -138,10 +139,10 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAd } } else if err == nil { // OpenSea could provide - for _, asset := range assetsContainer.Assets { - contractAddress := common.HexToAddress(asset.Contract.Address) + for _, collectible := range assetsContainer.Collectibles { + contractAddress := collectible.ID.ContractAddress balance := thirdparty.TokenBalance{ - TokenID: asset.TokenID, + TokenID: collectible.ID.TokenID, Balance: &bigint.BigInt{Int: big.NewInt(1)}, } ret[contractAddress] = append(ret[contractAddress], balance) @@ -154,13 +155,13 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAd return ret, nil } -func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*opensea.AssetContainer, error) { +func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit) if err != nil { return nil, err } - err = o.processAssets(chainID, assetContainer.Assets) + err = o.processAssets(assetContainer.Collectibles) if err != nil { return nil, err } @@ -168,13 +169,13 @@ func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner return assetContainer, nil } -func (o *Manager) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cursor string, limit int) (*opensea.AssetContainer, error) { +func (o *Manager) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { assetContainer, err := o.opensea.FetchAllAssetsByOwner(chainID, owner, cursor, limit) if err != nil { return nil, err } - err = o.processAssets(chainID, assetContainer.Assets) + err = o.processAssets(assetContainer.Collectibles) if err != nil { return nil, err } @@ -182,31 +183,25 @@ func (o *Manager) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cu return assetContainer, nil } -func (o *Manager) FetchAssetsByNFTUniqueID(chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID, limit int) (*opensea.AssetContainer, error) { - assetContainer := new(opensea.AssetContainer) - - idsToFetch := o.getIDsNotInCache(chainID, uniqueIDs) +func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) { + idsToFetch := o.getIDsNotInCollectiblesDataCache(uniqueIDs) if len(idsToFetch) > 0 { - fetchedAssetContainer, err := o.opensea.FetchAssetsByNFTUniqueID(chainID, idsToFetch, limit) + fetchedAssets, err := o.opensea.FetchAssetsByCollectibleUniqueID(idsToFetch) if err != nil { return nil, err } - err = o.processAssets(chainID, fetchedAssetContainer.Assets) + err = o.processAssets(fetchedAssets) if err != nil { return nil, err } - assetContainer.NextCursor = fetchedAssetContainer.NextCursor - assetContainer.PreviousCursor = fetchedAssetContainer.PreviousCursor } - assetContainer.Assets = o.getCachedAssets(chainID, uniqueIDs) - - return assetContainer, nil + return o.getCacheCollectiblesData(uniqueIDs), nil } -func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { +func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { mainFunc := func() (any, error) { return o.mainContractOwnershipProvider.FetchCollectibleOwnersByContractAddress(chainID, contractAddress) } @@ -224,15 +219,15 @@ func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID uint64, contra return owners.(*thirdparty.CollectibleContractOwnership), nil } -func isMetadataEmpty(asset opensea.Asset) bool { +func isMetadataEmpty(asset thirdparty.CollectibleData) bool { return asset.Name == "" && asset.Description == "" && asset.ImageURL == "" && asset.TokenURI == "" } -func (o *Manager) fetchTokenURI(chainID uint64, id thirdparty.CollectibleUniqueID) (string, error) { - backend, err := o.rpcClient.EthClient(chainID) +func (o *Manager) fetchTokenURI(id thirdparty.CollectibleUniqueID) (string, error) { + backend, err := o.rpcClient.EthClient(uint64(id.ChainID)) if err != nil { return "", err } @@ -262,25 +257,15 @@ func (o *Manager) fetchTokenURI(chainID uint64, id thirdparty.CollectibleUniqueI return tokenURI, err } -func (o *Manager) processAssets(chainID uint64, assets []opensea.Asset) error { - o.nftCacheLock.Lock() - defer o.nftCacheLock.Unlock() - - if _, ok := o.nftCache[chainID]; !ok { - o.nftCache[chainID] = make(map[string]opensea.Asset) - } - +func (o *Manager) processAssets(assets []thirdparty.CollectibleData) error { for idx, asset := range assets { - id := thirdparty.CollectibleUniqueID{ - ContractAddress: common.HexToAddress(asset.Contract.Address), - TokenID: asset.TokenID, - } + id := asset.ID if isMetadataEmpty(asset) { if o.metadataProvider == nil { return fmt.Errorf("CollectibleMetadataProvider not available") } - tokenURI, err := o.fetchTokenURI(chainID, id) + tokenURI, err := o.fetchTokenURI(id) if err != nil { return err @@ -288,70 +273,82 @@ func (o *Manager) processAssets(chainID uint64, assets []opensea.Asset) error { assets[idx].TokenURI = tokenURI - canProvide, err := o.metadataProvider.CanProvideCollectibleMetadata(chainID, id, tokenURI) + canProvide, err := o.metadataProvider.CanProvideCollectibleMetadata(id, tokenURI) if err != nil { return err } if canProvide { - metadata, err := o.metadataProvider.FetchCollectibleMetadata(chainID, id, tokenURI) + metadata, err := o.metadataProvider.FetchCollectibleMetadata(id, tokenURI) if err != nil { return err } if metadata != nil { - assets[idx].Name = metadata.Name - assets[idx].Description = metadata.Description - assets[idx].Collection.ImageURL = metadata.CollectionImageURL - assets[idx].ImageURL = metadata.ImageURL + assets[idx] = *metadata } } } - // The NFT description field could be arbitrarily large, causing memory management issues upstream. - // Trim it to a reasonable length here. - if len(assets[idx].Description) > maxNFTDescriptionLength { - assets[idx].Description = assets[idx].Description[:maxNFTDescriptionLength] - } - - o.nftCache[chainID][id.HashKey()] = assets[idx] + o.setCacheCollectibleData(assets[idx]) } return nil } -func (o *Manager) getIDsNotInCache(chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleUniqueID { +func (o *Manager) isIDInCollectiblesDataCache(id thirdparty.CollectibleUniqueID) bool { o.nftCacheLock.RLock() defer o.nftCacheLock.RUnlock() - - idsToFetch := make([]thirdparty.CollectibleUniqueID, 0, len(uniqueIDs)) - if _, ok := o.nftCache[chainID]; !ok { - idsToFetch = uniqueIDs - } else { - for _, id := range uniqueIDs { - if _, ok := o.nftCache[chainID][id.HashKey()]; !ok { - idsToFetch = append(idsToFetch, id) - } + if _, ok := o.nftCache[id.ChainID]; ok { + if _, ok := o.nftCache[id.ChainID][id.HashKey()]; ok { + return true } } + + return false +} + +func (o *Manager) getIDsNotInCollectiblesDataCache(uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleUniqueID { + idsToFetch := make([]thirdparty.CollectibleUniqueID, 0, len(uniqueIDs)) + for _, id := range uniqueIDs { + if o.isIDInCollectiblesDataCache(id) { + continue + } + idsToFetch = append(idsToFetch, id) + } return idsToFetch } -func (o *Manager) getCachedAssets(chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID) []opensea.Asset { +func (o *Manager) getCacheCollectiblesData(uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleData { o.nftCacheLock.RLock() defer o.nftCacheLock.RUnlock() - assets := make([]opensea.Asset, 0, len(uniqueIDs)) - - if _, ok := o.nftCache[chainID]; ok { - for _, id := range uniqueIDs { - - if asset, ok := o.nftCache[chainID][id.HashKey()]; ok { + assets := make([]thirdparty.CollectibleData, 0, len(uniqueIDs)) + for _, id := range uniqueIDs { + if _, ok := o.nftCache[id.ChainID]; ok { + if asset, ok := o.nftCache[id.ChainID][id.HashKey()]; ok { assets = append(assets, asset) + continue } } + emptyAsset := thirdparty.CollectibleData{ + ID: id, + } + assets = append(assets, emptyAsset) } - return assets } + +func (o *Manager) setCacheCollectibleData(data thirdparty.CollectibleData) { + o.nftCacheLock.Lock() + defer o.nftCacheLock.Unlock() + + id := data.ID + + if _, ok := o.nftCache[id.ChainID]; !ok { + o.nftCache[id.ChainID] = make(map[string]thirdparty.CollectibleData) + } + + o.nftCache[id.ChainID][id.HashKey()] = data +} diff --git a/services/wallet/common/const.go b/services/wallet/common/const.go index 99e2f3d97..2bef7e765 100644 --- a/services/wallet/common/const.go +++ b/services/wallet/common/const.go @@ -3,6 +3,7 @@ package common type ChainID uint64 const ( + UnknownChainID uint64 = 0 EthereumMainnet uint64 = 1 EthereumGoerli uint64 = 5 EthereumSepolia uint64 = 11155111 diff --git a/services/wallet/service.go b/services/wallet/service.go index 01e322619..e2484a817 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -29,6 +29,7 @@ import ( "github.com/status-im/status-go/services/wallet/thirdparty/coingecko" "github.com/status-im/status-go/services/wallet/thirdparty/cryptocompare" "github.com/status-im/status-go/services/wallet/thirdparty/infura" + "github.com/status-im/status-go/services/wallet/thirdparty/opensea" "github.com/status-im/status-go/services/wallet/token" "github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/walletevent" @@ -106,7 +107,8 @@ func NewService( alchemyClient := alchemy.NewClient(config.WalletConfig.AlchemyAPIKeys) infuraClient := infura.NewClient(config.WalletConfig.InfuraAPIKey, config.WalletConfig.InfuraAPIKeySecret) - collectiblesManager := collectibles.NewManager(rpcClient, alchemyClient, infuraClient, config.WalletConfig.OpenseaAPIKey, walletFeed) + openseaClient := opensea.NewClient(config.WalletConfig.OpenseaAPIKey, walletFeed) + collectiblesManager := collectibles.NewManager(rpcClient, alchemyClient, infuraClient, openseaClient) return &Service{ db: db, accountsDB: accountsDB, diff --git a/services/wallet/thirdparty/alchemy/client.go b/services/wallet/thirdparty/alchemy/client.go index 83884d276..ea4e1d0d7 100644 --- a/services/wallet/thirdparty/alchemy/client.go +++ b/services/wallet/thirdparty/alchemy/client.go @@ -15,8 +15,8 @@ import ( "github.com/status-im/status-go/services/wallet/thirdparty" ) -func getBaseURL(chainID uint64) (string, error) { - switch chainID { +func getBaseURL(chainID walletCommon.ChainID) (string, error) { + switch uint64(chainID) { case walletCommon.EthereumMainnet: return "https://eth-mainnet.g.alchemy.com", nil case walletCommon.EthereumGoerli: @@ -43,7 +43,7 @@ func getAPIKeySubpath(apiKey string) string { return apiKey } -func getNFTBaseURL(chainID uint64, apiKey string) (string, error) { +func getNFTBaseURL(chainID walletCommon.ChainID, apiKey string) (string, error) { baseURL, err := getBaseURL(chainID) if err != nil { @@ -93,7 +93,7 @@ func (o *Client) doQuery(url string) (*http.Response, error) { return resp, nil } -func (o *Client) IsChainSupported(chainID uint64) bool { +func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool { _, err := getBaseURL(chainID) return err == nil } @@ -125,13 +125,13 @@ func alchemyOwnershipToCommon(contractAddress common.Address, alchemyOwnership C return &ownership, nil } -func (o *Client) FetchCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { +func (o *Client) FetchCollectibleOwnersByContractAddress(chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { queryParams := url.Values{ "contractAddress": {contractAddress.String()}, "withTokenBalances": {"true"}, } - url, err := getNFTBaseURL(chainID, o.apiKeys[chainID]) + url, err := getNFTBaseURL(chainID, o.apiKeys[uint64(chainID)]) if err != nil { return nil, err diff --git a/services/wallet/thirdparty/collectible_types.go b/services/wallet/thirdparty/collectible_types.go index edcb6dc51..0594094dc 100644 --- a/services/wallet/thirdparty/collectible_types.go +++ b/services/wallet/thirdparty/collectible_types.go @@ -1,29 +1,115 @@ package thirdparty import ( + "fmt" + "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/services/wallet/bigint" + w_common "github.com/status-im/status-go/services/wallet/common" ) type CollectibleUniqueID struct { - ContractAddress common.Address `json:"contractAddress"` - TokenID *bigint.BigInt `json:"tokenID"` + ChainID w_common.ChainID `json:"chainID"` + ContractAddress common.Address `json:"contractAddress"` + TokenID *bigint.BigInt `json:"tokenID"` } func (k *CollectibleUniqueID) HashKey() string { - return k.ContractAddress.String() + "+" + k.TokenID.String() + return fmt.Sprintf("%d+%s+%s", k.ChainID, k.ContractAddress.String(), k.TokenID.String()) } -type CollectibleMetadata struct { - Name string `json:"name"` - Description string `json:"description"` - CollectionImageURL string `json:"collection_image"` - ImageURL string `json:"image"` +func GroupCollectibleUIDsByChainID(uids []CollectibleUniqueID) map[w_common.ChainID][]CollectibleUniqueID { + ret := make(map[w_common.ChainID][]CollectibleUniqueID) + + for _, uid := range uids { + if _, ok := ret[uid.ChainID]; !ok { + ret[uid.ChainID] = make([]CollectibleUniqueID, 0, len(uids)) + } + ret[uid.ChainID] = append(ret[uid.ChainID], uid) + } + + return ret +} + +type CollectionTrait struct { + Min float64 `json:"min"` + Max float64 `json:"max"` +} + +type CollectionData struct { + Name string `json:"name"` + Slug string `json:"slug"` + ImageURL string `json:"image_url"` + Traits map[string]CollectionTrait `json:"traits"` +} + +type CollectibleTrait struct { + TraitType string `json:"trait_type"` + Value string `json:"value"` + DisplayType string `json:"display_type"` + MaxValue string `json:"max_value"` +} + +type CollectibleData struct { + ID CollectibleUniqueID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Permalink string `json:"permalink"` + ImageURL string `json:"image_url"` + AnimationURL string `json:"animation_url"` + AnimationMediaType string `json:"animation_media_type"` + Traits []CollectibleTrait `json:"traits"` + BackgroundColor string `json:"background_color"` + TokenURI string `json:"token_uri"` + CollectionData CollectionData `json:"collection_data"` +} + +type CollectibleHeader struct { + ID CollectibleUniqueID `json:"id"` + Name string `json:"name"` + ImageURL string `json:"image_url"` + AnimationURL string `json:"animation_url"` + AnimationMediaType string `json:"animation_media_type"` + BackgroundColor string `json:"background_color"` + CollectionName string `json:"collection_name"` +} + +type CollectibleDataContainer struct { + Collectibles []CollectibleData + NextCursor string + PreviousCursor string +} + +func (c *CollectibleData) toHeader() CollectibleHeader { + return CollectibleHeader{ + ID: c.ID, + Name: c.Name, + ImageURL: c.ImageURL, + AnimationURL: c.AnimationURL, + AnimationMediaType: c.AnimationMediaType, + BackgroundColor: c.BackgroundColor, + CollectionName: c.CollectionData.Name, + } +} + +func CollectiblesToHeaders(collectibles []CollectibleData) []CollectibleHeader { + res := make([]CollectibleHeader, 0, len(collectibles)) + + for _, c := range collectibles { + res = append(res, c.toHeader()) + } + + return res +} + +type CollectibleOwnershipProvider interface { + CanProvideAccountOwnership(chainID uint64) (bool, error) + FetchAccountOwnership(chainID uint64, address common.Address) (*CollectibleData, error) } type CollectibleMetadataProvider interface { - CanProvideCollectibleMetadata(chainID uint64, id CollectibleUniqueID, tokenURI string) (bool, error) - FetchCollectibleMetadata(chainID uint64, id CollectibleUniqueID, tokenURI string) (*CollectibleMetadata, error) + CanProvideCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (bool, error) + FetchCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (*CollectibleData, error) } type TokenBalance struct { @@ -44,6 +130,6 @@ type CollectibleContractOwnership struct { } type CollectibleContractOwnershipProvider interface { - FetchCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*CollectibleContractOwnership, error) - IsChainSupported(chainID uint64) bool + FetchCollectibleOwnersByContractAddress(chainID w_common.ChainID, contractAddress common.Address) (*CollectibleContractOwnership, error) + IsChainSupported(chainID w_common.ChainID) bool } diff --git a/services/wallet/thirdparty/infura/client.go b/services/wallet/thirdparty/infura/client.go index f19fbeac3..aabc36401 100644 --- a/services/wallet/thirdparty/infura/client.go +++ b/services/wallet/thirdparty/infura/client.go @@ -63,8 +63,8 @@ func (o *Client) doQuery(url string) (*http.Response, error) { return resp, nil } -func (o *Client) IsChainSupported(chainID uint64) bool { - switch chainID { +func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool { + switch uint64(chainID) { case walletCommon.EthereumMainnet, walletCommon.EthereumGoerli, walletCommon.EthereumSepolia, walletCommon.ArbitrumMainnet: return true } @@ -98,7 +98,7 @@ func infuraOwnershipToCommon(contractAddress common.Address, ownersMap map[commo return &ownership, nil } -func (o *Client) FetchCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { +func (o *Client) FetchCollectibleOwnersByContractAddress(chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { cursor := "" ownersMap := make(map[common.Address][]CollectibleOwner) diff --git a/services/wallet/thirdparty/opensea/client.go b/services/wallet/thirdparty/opensea/client.go index 7e7bb8d22..02e5a6027 100644 --- a/services/wallet/thirdparty/opensea/client.go +++ b/services/wallet/thirdparty/opensea/client.go @@ -16,6 +16,9 @@ import ( "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "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/connection" @@ -36,12 +39,17 @@ const GetRequestWaitTime = 300 * time.Millisecond const ChainIDRequiringAPIKey = walletCommon.EthereumMainnet +const FetchNoLimit = 0 + var ( ErrChainIDNotSupported = errors.New("chainID not supported by opensea API") ) -func getbaseURL(chainID uint64) (string, error) { - switch chainID { +type urlGetter func(walletCommon.ChainID, string) (string, error) + +func getbaseURL(chainID walletCommon.ChainID) (string, error) { + // v1 Endpoints only support L1 chain + switch uint64(chainID) { case walletCommon.EthereumMainnet: return "https://api.opensea.io/api/v1", nil case walletCommon.EthereumGoerli: @@ -51,6 +59,34 @@ func getbaseURL(chainID uint64) (string, error) { return "", ErrChainIDNotSupported } +func getURL(chainID walletCommon.ChainID, path string) (string, error) { + baseURL, err := getbaseURL(chainID) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s/%s", baseURL, path), nil +} + +func chainStringToChainID(chainString string) walletCommon.ChainID { + chainID := walletCommon.UnknownChainID + switch chainString { + case "ethereum": + chainID = walletCommon.EthereumMainnet + case "arbitrum": + chainID = walletCommon.ArbitrumMainnet + case "optimism": + chainID = walletCommon.OptimismMainnet + case "goerli": + chainID = walletCommon.EthereumGoerli + case "arbitrum_goerli": + chainID = walletCommon.ArbitrumGoerli + case "optimism_goerli": + chainID = walletCommon.OptimismGoerli + } + return walletCommon.ChainID(chainID) +} + type TraitValue string func (st *TraitValue) UnmarshalJSON(b []byte) error { @@ -78,7 +114,8 @@ type AssetContainer struct { } type Contract struct { - Address string `json:"address"` + Address string `json:"address"` + ChainIdentifier string `json:"chain_identifier"` } type Trait struct { @@ -143,6 +180,62 @@ type OwnedCollection struct { OwnedAssetCount *bigint.BigInt `json:"owned_asset_count"` } +func (c *Asset) id() thirdparty.CollectibleUniqueID { + return thirdparty.CollectibleUniqueID{ + ChainID: chainStringToChainID(c.Contract.ChainIdentifier), + ContractAddress: common.HexToAddress(c.Contract.Address), + TokenID: c.TokenID, + } +} + +func openseaToCollectibleTraits(traits []Trait) []thirdparty.CollectibleTrait { + ret := make([]thirdparty.CollectibleTrait, 0, len(traits)) + caser := cases.Title(language.Und, cases.NoLower) + for _, orig := range traits { + dest := thirdparty.CollectibleTrait{ + TraitType: strings.Replace(orig.TraitType, "_", " ", 1), + Value: caser.String(string(orig.Value)), + DisplayType: orig.DisplayType, + MaxValue: orig.MaxValue, + } + + ret = append(ret, dest) + } + return ret +} + +func (c *Collection) toCommon() thirdparty.CollectionData { + ret := thirdparty.CollectionData{ + Name: c.Name, + Slug: c.Slug, + ImageURL: c.ImageURL, + Traits: make(map[string]thirdparty.CollectionTrait), + } + for traitType, trait := range c.Traits { + ret.Traits[traitType] = thirdparty.CollectionTrait{ + Min: trait.Min, + Max: trait.Max, + } + } + return ret +} + +func (c *Asset) toCommon() thirdparty.CollectibleData { + return thirdparty.CollectibleData{ + ID: c.id(), + Name: c.Name, + Description: c.Description, + Permalink: c.Permalink, + ImageURL: c.ImageURL, + AnimationURL: c.AnimationURL, + AnimationMediaType: c.AnimationMediaType, + Traits: openseaToCollectibleTraits(c.Traits), + BackgroundColor: c.BackgroundColor, + TokenURI: c.TokenURI, + CollectionData: c.Collection.toCommon(), + } +} + type HTTPClient struct { client *http.Client getRequestLock sync.RWMutex @@ -242,6 +335,7 @@ type Client struct { client *HTTPClient apiKey string connectionStatus *connection.Status + urlGetter urlGetter } // new opensea client. @@ -250,20 +344,21 @@ func NewClient(apiKey string, feed *event.Feed) *Client { client: newHTTPClient(), apiKey: apiKey, connectionStatus: connection.NewStatus(EventCollectibleStatusChanged, feed), + urlGetter: getURL, } } -func (o *Client) FetchAllCollectionsByOwner(chainID uint64, owner common.Address) ([]OwnedCollection, error) { +func (o *Client) FetchAllCollectionsByOwner(chainID walletCommon.ChainID, owner common.Address) ([]OwnedCollection, error) { offset := 0 var collections []OwnedCollection - baseURL, err := getbaseURL(chainID) - if err != nil { - return nil, err - } - for { - url := fmt.Sprintf("%s/collections?asset_owner=%s&offset=%d&limit=%d", baseURL, owner, offset, CollectionLimit) + path := fmt.Sprintf("collections?asset_owner=%s&offset=%d&limit=%d", owner, offset, CollectionLimit) + url, err := o.urlGetter(chainID, path) + if err != nil { + return nil, err + } + body, err := o.client.doGetRequest(url, o.apiKey) if err != nil { o.connectionStatus.SetIsConnected(false) @@ -291,7 +386,7 @@ func (o *Client) FetchAllCollectionsByOwner(chainID uint64, owner common.Address return collections, nil } -func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner common.Address, collectionSlug string, cursor string, limit int) (*AssetContainer, error) { +func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { queryParams := url.Values{ "owner": {owner.String()}, "collection": {collectionSlug}, @@ -304,7 +399,7 @@ func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner common return o.fetchAssets(chainID, queryParams, limit) } -func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*AssetContainer, error) { +func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { queryParams := url.Values{ "owner": {owner.String()}, } @@ -320,7 +415,7 @@ func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner c return o.fetchAssets(chainID, queryParams, limit) } -func (o *Client) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cursor string, limit int) (*AssetContainer, error) { +func (o *Client) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) { queryParams := url.Values{ "owner": {owner.String()}, } @@ -332,18 +427,107 @@ func (o *Client) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cur return o.fetchAssets(chainID, queryParams, limit) } -func (o *Client) FetchAssetsByNFTUniqueID(chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID, limit int) (*AssetContainer, error) { +func (o *Client) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) { queryParams := url.Values{} - for _, uniqueID := range uniqueIDs { - queryParams.Add("token_ids", uniqueID.TokenID.String()) - queryParams.Add("asset_contract_addresses", uniqueID.ContractAddress.String()) + ret := make([]thirdparty.CollectibleData, 0, len(uniqueIDs)) + + idsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(uniqueIDs) + for chainID, ids := range idsPerChainID { + for _, id := range ids { + queryParams.Add("token_ids", id.TokenID.String()) + queryParams.Add("asset_contract_addresses", id.ContractAddress.String()) + } + + data, err := o.fetchAssets(chainID, queryParams, FetchNoLimit) + if err != nil { + return nil, err + } + + ret = append(ret, data.Collectibles...) } - return o.fetchAssets(chainID, queryParams, limit) + return ret, nil } -func (o *Client) fetchAssets(chainID uint64, queryParams url.Values, limit int) (*AssetContainer, error) { +func (o *Client) fetchAssets(chainID walletCommon.ChainID, queryParams url.Values, limit int) (*thirdparty.CollectibleDataContainer, error) { + assets := new(thirdparty.CollectibleDataContainer) + + if len(queryParams["cursor"]) > 0 { + assets.PreviousCursor = queryParams["cursor"][0] + } + + tmpLimit := AssetLimit + if limit > FetchNoLimit && limit < tmpLimit { + tmpLimit = limit + } + + queryParams["limit"] = []string{strconv.Itoa(tmpLimit)} + for { + path := "assets?" + queryParams.Encode() + url, err := o.urlGetter(chainID, path) + if err != nil { + return nil, err + } + + body, err := o.client.doGetRequest(url, o.apiKey) + if err != nil { + o.connectionStatus.SetIsConnected(false) + return nil, err + } + o.connectionStatus.SetIsConnected(true) + + // if Json is not returned there must be an error + if !json.Valid(body) { + return nil, fmt.Errorf("invalid json: %s", string(body)) + } + + container := AssetContainer{} + err = json.Unmarshal(body, &container) + if err != nil { + return nil, err + } + + for _, asset := range container.Assets { + if len(asset.AnimationURL) > 0 { + asset.AnimationMediaType, err = o.client.doContentTypeRequest(asset.AnimationURL) + if err != nil { + asset.AnimationURL = "" + } + } + assets.Collectibles = append(assets.Collectibles, asset.toCommon()) + } + assets.NextCursor = container.NextCursor + + if len(assets.NextCursor) == 0 { + break + } + + queryParams["cursor"] = []string{assets.NextCursor} + + if limit > FetchNoLimit && len(assets.Collectibles) >= limit { + break + } + } + + return assets, nil +} + +// Only here for compatibility with mobile app, to be removed +func (o *Client) FetchAllOpenseaAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*AssetContainer, error) { + queryParams := url.Values{ + "owner": {owner.String()}, + "collection": {collectionSlug}, + } + + if len(cursor) > 0 { + queryParams["cursor"] = []string{cursor} + } + + return o.fetchOpenseaAssets(chainID, queryParams, limit) +} + +func (o *Client) fetchOpenseaAssets(chainID walletCommon.ChainID, queryParams url.Values, limit int) (*AssetContainer, error) { assets := new(AssetContainer) if len(queryParams["cursor"]) > 0 { diff --git a/services/wallet/thirdparty/opensea/client_test.go b/services/wallet/thirdparty/opensea/client_test.go index 37b1eb812..96fa5a404 100644 --- a/services/wallet/thirdparty/opensea/client_test.go +++ b/services/wallet/thirdparty/opensea/client_test.go @@ -9,6 +9,9 @@ import ( "testing" "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/connection" + "github.com/status-im/status-go/services/wallet/thirdparty" "github.com/stretchr/testify/assert" @@ -20,8 +23,27 @@ const ( ExpectedExpiredKeyError = "invalid json: Expired API key" ) +func initTestClient(srv *httptest.Server) *Client { + urlGetter := func(chainID walletCommon.ChainID, path string) (string, error) { + return srv.URL, nil + } + + status := connection.NewStatus("", nil) + + client := &HTTPClient{ + client: srv.Client(), + } + opensea := &Client{ + client: client, + connectionStatus: status, + urlGetter: urlGetter, + } + + return opensea +} + func TestFetchAllCollectionsByOwner(t *testing.T) { - expected := []OwnedCollection{{ + expectedOS := []OwnedCollection{{ Collection: Collection{ Name: "Rocky", Slug: "rocky", @@ -29,7 +51,7 @@ func TestFetchAllCollectionsByOwner(t *testing.T) { }, OwnedAssetCount: &bigint.BigInt{Int: big.NewInt(1)}, }} - response, _ := json.Marshal(expected) + response, _ := json.Marshal(expectedOS) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) _, err := w.Write(response) @@ -39,15 +61,9 @@ func TestFetchAllCollectionsByOwner(t *testing.T) { })) defer srv.Close() - client := &HTTPClient{ - client: srv.Client(), - } - opensea := &Client{ - client: client, - url: srv.URL, - } - res, err := opensea.FetchAllCollectionsByOwner(common.Address{1}) - assert.Equal(t, expected, res) + opensea := initTestClient(srv) + res, err := opensea.FetchAllCollectionsByOwner(walletCommon.ChainID(1), common.Address{1}) + assert.Equal(t, expectedOS, res) assert.Nil(t, err) } @@ -61,34 +77,56 @@ func TestFetchAllCollectionsByOwnerWithInValidJson(t *testing.T) { })) defer srv.Close() - client := &HTTPClient{ - client: srv.Client(), - } - opensea := &Client{ - client: client, - url: srv.URL, - } - res, err := opensea.FetchAllCollectionsByOwner(common.Address{1}) + opensea := initTestClient(srv) + res, err := opensea.FetchAllCollectionsByOwner(walletCommon.ChainID(1), common.Address{1}) assert.Nil(t, res) assert.Equal(t, err, fmt.Errorf(ExpectedExpiredKeyError)) } func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) { - expected := AssetContainer{ + expectedOS := AssetContainer{ Assets: []Asset{{ ID: 1, + TokenID: &bigint.BigInt{Int: big.NewInt(1)}, Name: "Rocky", Description: "Rocky Balboa", Permalink: "permalink", ImageThumbnailURL: "ImageThumbnailURL", ImageURL: "ImageUrl", - Contract: Contract{Address: "1"}, - Collection: Collection{Name: "Rocky"}, + Contract: Contract{ + Address: "1", + ChainIdentifier: "ethereum", + }, + Collection: Collection{ + Name: "Rocky", + Traits: map[string]CollectionTrait{}, + }, + Traits: []Trait{}, }}, NextCursor: "", PreviousCursor: "", } - response, _ := json.Marshal(expected) + expectedCommon := thirdparty.CollectibleDataContainer{ + Collectibles: []thirdparty.CollectibleData{{ + ID: thirdparty.CollectibleUniqueID{ + ChainID: 1, + ContractAddress: common.HexToAddress("0x1"), + TokenID: &bigint.BigInt{Int: big.NewInt(1)}, + }, + Name: "Rocky", + Description: "Rocky Balboa", + Permalink: "permalink", + ImageURL: "ImageUrl", + Traits: []thirdparty.CollectibleTrait{}, + CollectionData: thirdparty.CollectionData{ + Name: "Rocky", + Traits: map[string]thirdparty.CollectionTrait{}, + }, + }}, + NextCursor: "", + PreviousCursor: "", + } + response, _ := json.Marshal(expectedOS) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) _, err := w.Write(response) @@ -98,16 +136,10 @@ func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) { })) defer srv.Close() - client := &HTTPClient{ - client: srv.Client(), - } - opensea := &Client{ - client: client, - url: srv.URL, - } - res, err := opensea.FetchAllAssetsByOwnerAndCollection(common.Address{1}, "rocky", "", 200) + opensea := initTestClient(srv) + res, err := opensea.FetchAllAssetsByOwnerAndCollection(walletCommon.ChainID(1), common.Address{1}, "rocky", "", 200) assert.Nil(t, err) - assert.Equal(t, expected, *res) + assert.Equal(t, expectedCommon, *res) } func TestFetchAllAssetsByOwnerAndCollectionInvalidJson(t *testing.T) { @@ -120,14 +152,8 @@ func TestFetchAllAssetsByOwnerAndCollectionInvalidJson(t *testing.T) { })) defer srv.Close() - client := &HTTPClient{ - client: srv.Client(), - } - opensea := &Client{ - client: client, - url: srv.URL, - } - res, err := opensea.FetchAllAssetsByOwnerAndCollection(common.Address{1}, "rocky", "", 200) + opensea := initTestClient(srv) + res, err := opensea.FetchAllAssetsByOwnerAndCollection(walletCommon.ChainID(1), common.Address{1}, "rocky", "", 200) assert.Nil(t, res) assert.Equal(t, fmt.Errorf(ExpectedExpiredKeyError), err) }