chore: make opensea client return common types

This commit is contained in:
Dario Gabriel Lipicar 2023-07-18 12:01:53 -03:00 committed by dlipicar
parent 383de2a7df
commit b1cf54974e
12 changed files with 505 additions and 192 deletions

View File

@ -143,7 +143,7 @@ func (m *DefaultTokenManager) GetAllChainIDs() ([]uint64, error) {
} }
type CollectiblesManager interface { 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) { 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: 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 { if _, exists := ownedERC721Tokens[chainID]; !exists || len(ownedERC721Tokens[chainID]) == 0 {
continue chainIDLoopERC721 continue chainIDLoopERC721
} }
@ -2038,7 +2039,7 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss
continue continue
} }
tokenBalances := ownedERC721Tokens[chainID][account][gethcommon.HexToAddress(address)] tokenBalances := ownedERC721Tokens[chainID][account][contractAddress]
if len(tokenBalances) > 0 { if len(tokenBalances) > 0 {
// 'account' owns some TokenID owned from contract 'address' // 'account' owns some TokenID owned from contract 'address'
if _, exists := accountsChainIDsCombinations[account]; !exists { if _, exists := accountsChainIDsCombinations[account]; !exists {
@ -2189,8 +2190,6 @@ func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tok
ownedERC721Tokens := make(CollectiblesByChain) ownedERC721Tokens := make(CollectiblesByChain)
client := m.openseaClientBuilder.NewOpenseaClient(m.walletConfig.OpenseaAPIKey, nil)
for chainID, erc721Tokens := range tokenRequirements { for chainID, erc721Tokens := range tokenRequirements {
skipChain := true skipChain := true
@ -2214,7 +2213,7 @@ func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tok
} }
for _, owner := range walletAddresses { 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 { if err != nil {
m.logger.Info("couldn't fetch owner assets", zap.Error(err)) m.logger.Info("couldn't fetch owner assets", zap.Error(err))
return nil, err return nil, err

View File

@ -21,6 +21,7 @@ import (
"github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/transport" "github.com/status-im/status-go/protocol/transport"
"github.com/status-im/status-go/services/wallet/bigint" "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"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
@ -102,8 +103,8 @@ func (m *testCollectiblesManager) setResponse(chainID uint64, walletAddress geth
m.response[chainID][walletAddress][contractAddress] = balances m.response[chainID][walletAddress][contractAddress] = balances
} }
func (m *testCollectiblesManager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) { func (m *testCollectiblesManager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.ChainID, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
return m.response[chainID][ownerAddress], nil return m.response[uint64(chainID)][ownerAddress], nil
} }
type testTokenManager struct { type testTokenManager struct {

View File

@ -545,12 +545,12 @@ func tokenURIToCommunityID(tokenURI string) string {
return communityID 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) != "" ret := tokenURI != "" && tokenURIToCommunityID(tokenURI) != ""
return ret, nil 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 { if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready") return nil, fmt.Errorf("messenger not ready")
} }
@ -573,12 +573,16 @@ func (s *Service) FetchCollectibleMetadata(chainID uint64, id thirdparty.Collect
for _, tokenMetadata := range tokensMetadata { for _, tokenMetadata := range tokensMetadata {
contractAddresses := tokenMetadata.GetContractAddresses() contractAddresses := tokenMetadata.GetContractAddresses()
if contractAddresses[chainID] == id.ContractAddress.Hex() { if contractAddresses[uint64(id.ChainID)] == id.ContractAddress.Hex() {
return &thirdparty.CollectibleMetadata{ return &thirdparty.CollectibleData{
Name: tokenMetadata.GetName(), ID: id,
Description: tokenMetadata.GetDescription(), Name: tokenMetadata.GetName(),
CollectionImageURL: tokenMetadata.GetImage(), Description: tokenMetadata.GetDescription(),
ImageURL: tokenMetadata.GetImage(), ImageURL: tokenMetadata.GetImage(),
CollectionData: thirdparty.CollectionData{
Name: tokenMetadata.GetName(),
ImageURL: tokenMetadata.GetImage(),
},
}, nil }, nil
} }
} }

View File

@ -299,13 +299,27 @@ func (api *API) GetCryptoOnRamps(ctx context.Context) ([]CryptoOnRamp, error) {
return api.s.cryptoOnRampManager.Get() 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) return api.s.collectiblesManager.FetchAllCollectionsByOwner(chainID, owner)
} }
// Kept for compatibility with mobile app func (api *API) GetOpenseaAssetsByOwnerAndCollectionWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*opensea.AssetContainer, error) {
func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainID uint64, owner common.Address, collectionSlug string, limit int) ([]opensea.Asset, 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) container, err := api.GetOpenseaAssetsByOwnerAndCollectionWithCursor(ctx, chainID, owner, collectionSlug, "", limit)
if err != nil { if err != nil {
return nil, err return nil, err
@ -313,35 +327,34 @@ func (api *API) GetOpenseaAssetsByOwnerAndCollection(ctx context.Context, chainI
return container.Assets, nil 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) { 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 get opensea assets") log.Debug("call to GetCollectiblesByOwnerAndCollectionWithCursor")
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit) 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) { func (api *API) GetCollectiblesByOwnerWithCursor(ctx context.Context, chainID wcommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
log.Debug("call to FetchAllAssetsByOwner") log.Debug("call to GetCollectiblesByOwnerWithCursor")
return api.s.collectiblesManager.FetchAllAssetsByOwner(chainID, owner, cursor, limit) 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) { 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 GetOpenseaAssetsByOwnerAndContractAddressWithCursor") log.Debug("call to GetCollectiblesByOwnerAndContractAddressWithCursor")
return api.s.collectiblesManager.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit) 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) { func (api *API) GetCollectiblesByUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) {
log.Debug("call to GetOpenseaAssetsByNFTUniqueID") log.Debug("call to GetCollectiblesByUniqueID")
return api.s.collectiblesManager.FetchAssetsByNFTUniqueID(chainID, uniqueIDs, limit) 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") log.Debug("call to GetCollectibleOwnersByContractAddress")
return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(chainID, contractAddress) 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") Collectibles API End
return api.s.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses) */
}
func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error { func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error {
log.Debug("call to AddEthereumChain") log.Debug("call to AddEthereumChain")

View File

@ -12,10 +12,10 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "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/contracts/collectibles"
"github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/wallet/bigint" "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"
"github.com/status-im/status-go/services/wallet/thirdparty/opensea" "github.com/status-im/status-go/services/wallet/thirdparty/opensea"
) )
@ -24,8 +24,6 @@ const requestTimeout = 5 * time.Second
const hystrixContractOwnershipClientName = "contractOwnershipClient" const hystrixContractOwnershipClientName = "contractOwnershipClient"
const maxNFTDescriptionLength = 1024
// ERC721 does not support function "TokenURI" if call // ERC721 does not support function "TokenURI" if call
// returns error starting with one of these strings // returns error starting with one of these strings
var noTokenURIErrorPrefixes = []string{ var noTokenURIErrorPrefixes = []string{
@ -39,11 +37,11 @@ type Manager struct {
fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider
metadataProvider thirdparty.CollectibleMetadataProvider metadataProvider thirdparty.CollectibleMetadataProvider
opensea *opensea.Client opensea *opensea.Client
nftCache map[uint64]map[string]opensea.Asset nftCache map[walletCommon.ChainID]map[string]thirdparty.CollectibleData
nftCacheLock sync.RWMutex 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{ hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{
Timeout: 10000, Timeout: 10000,
MaxConcurrentRequests: 100, MaxConcurrentRequests: 100,
@ -55,8 +53,7 @@ func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.
rpcClient: rpcClient, rpcClient: rpcClient,
mainContractOwnershipProvider: mainContractOwnershipProvider, mainContractOwnershipProvider: mainContractOwnershipProvider,
fallbackContractOwnershipProvider: fallbackContractOwnershipProvider, fallbackContractOwnershipProvider: fallbackContractOwnershipProvider,
opensea: opensea.NewClient(openseaAPIKey, walletFeed), opensea: opensea,
nftCache: make(map[uint64]map[string]opensea.Asset),
} }
} }
@ -94,17 +91,21 @@ func (o *Manager) SetMetadataProvider(metadataProvider thirdparty.CollectibleMet
o.metadataProvider = metadataProvider 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) 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) assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = o.processAssets(chainID, assetContainer.Assets) err = o.processAssets(assetContainer.Collectibles)
if err != nil { if err != nil {
return nil, err 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 // 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) ret := make(thirdparty.TokenBalancesPerContractAddress)
for _, contractAddress := range contractAddresses { for _, contractAddress := range contractAddresses {
@ -138,10 +139,10 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAd
} }
} else if err == nil { } else if err == nil {
// OpenSea could provide // OpenSea could provide
for _, asset := range assetsContainer.Assets { for _, collectible := range assetsContainer.Collectibles {
contractAddress := common.HexToAddress(asset.Contract.Address) contractAddress := collectible.ID.ContractAddress
balance := thirdparty.TokenBalance{ balance := thirdparty.TokenBalance{
TokenID: asset.TokenID, TokenID: collectible.ID.TokenID,
Balance: &bigint.BigInt{Int: big.NewInt(1)}, Balance: &bigint.BigInt{Int: big.NewInt(1)},
} }
ret[contractAddress] = append(ret[contractAddress], balance) ret[contractAddress] = append(ret[contractAddress], balance)
@ -154,13 +155,13 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAd
return ret, nil 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) assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = o.processAssets(chainID, assetContainer.Assets) err = o.processAssets(assetContainer.Collectibles)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -168,13 +169,13 @@ func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner
return assetContainer, nil 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) assetContainer, err := o.opensea.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = o.processAssets(chainID, assetContainer.Assets) err = o.processAssets(assetContainer.Collectibles)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -182,31 +183,25 @@ func (o *Manager) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cu
return assetContainer, nil return assetContainer, nil
} }
func (o *Manager) FetchAssetsByNFTUniqueID(chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID, limit int) (*opensea.AssetContainer, error) { func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) {
assetContainer := new(opensea.AssetContainer) idsToFetch := o.getIDsNotInCollectiblesDataCache(uniqueIDs)
idsToFetch := o.getIDsNotInCache(chainID, uniqueIDs)
if len(idsToFetch) > 0 { if len(idsToFetch) > 0 {
fetchedAssetContainer, err := o.opensea.FetchAssetsByNFTUniqueID(chainID, idsToFetch, limit) fetchedAssets, err := o.opensea.FetchAssetsByCollectibleUniqueID(idsToFetch)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = o.processAssets(chainID, fetchedAssetContainer.Assets) err = o.processAssets(fetchedAssets)
if err != nil { if err != nil {
return nil, err return nil, err
} }
assetContainer.NextCursor = fetchedAssetContainer.NextCursor
assetContainer.PreviousCursor = fetchedAssetContainer.PreviousCursor
} }
assetContainer.Assets = o.getCachedAssets(chainID, uniqueIDs) return o.getCacheCollectiblesData(uniqueIDs), nil
return assetContainer, 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) { mainFunc := func() (any, error) {
return o.mainContractOwnershipProvider.FetchCollectibleOwnersByContractAddress(chainID, contractAddress) return o.mainContractOwnershipProvider.FetchCollectibleOwnersByContractAddress(chainID, contractAddress)
} }
@ -224,15 +219,15 @@ func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID uint64, contra
return owners.(*thirdparty.CollectibleContractOwnership), nil return owners.(*thirdparty.CollectibleContractOwnership), nil
} }
func isMetadataEmpty(asset opensea.Asset) bool { func isMetadataEmpty(asset thirdparty.CollectibleData) bool {
return asset.Name == "" && return asset.Name == "" &&
asset.Description == "" && asset.Description == "" &&
asset.ImageURL == "" && asset.ImageURL == "" &&
asset.TokenURI == "" asset.TokenURI == ""
} }
func (o *Manager) fetchTokenURI(chainID uint64, id thirdparty.CollectibleUniqueID) (string, error) { func (o *Manager) fetchTokenURI(id thirdparty.CollectibleUniqueID) (string, error) {
backend, err := o.rpcClient.EthClient(chainID) backend, err := o.rpcClient.EthClient(uint64(id.ChainID))
if err != nil { if err != nil {
return "", err return "", err
} }
@ -262,25 +257,15 @@ func (o *Manager) fetchTokenURI(chainID uint64, id thirdparty.CollectibleUniqueI
return tokenURI, err return tokenURI, err
} }
func (o *Manager) processAssets(chainID uint64, assets []opensea.Asset) error { func (o *Manager) processAssets(assets []thirdparty.CollectibleData) error {
o.nftCacheLock.Lock()
defer o.nftCacheLock.Unlock()
if _, ok := o.nftCache[chainID]; !ok {
o.nftCache[chainID] = make(map[string]opensea.Asset)
}
for idx, asset := range assets { for idx, asset := range assets {
id := thirdparty.CollectibleUniqueID{ id := asset.ID
ContractAddress: common.HexToAddress(asset.Contract.Address),
TokenID: asset.TokenID,
}
if isMetadataEmpty(asset) { if isMetadataEmpty(asset) {
if o.metadataProvider == nil { if o.metadataProvider == nil {
return fmt.Errorf("CollectibleMetadataProvider not available") return fmt.Errorf("CollectibleMetadataProvider not available")
} }
tokenURI, err := o.fetchTokenURI(chainID, id) tokenURI, err := o.fetchTokenURI(id)
if err != nil { if err != nil {
return err return err
@ -288,70 +273,82 @@ func (o *Manager) processAssets(chainID uint64, assets []opensea.Asset) error {
assets[idx].TokenURI = tokenURI assets[idx].TokenURI = tokenURI
canProvide, err := o.metadataProvider.CanProvideCollectibleMetadata(chainID, id, tokenURI) canProvide, err := o.metadataProvider.CanProvideCollectibleMetadata(id, tokenURI)
if err != nil { if err != nil {
return err return err
} }
if canProvide { if canProvide {
metadata, err := o.metadataProvider.FetchCollectibleMetadata(chainID, id, tokenURI) metadata, err := o.metadataProvider.FetchCollectibleMetadata(id, tokenURI)
if err != nil { if err != nil {
return err return err
} }
if metadata != nil { if metadata != nil {
assets[idx].Name = metadata.Name assets[idx] = *metadata
assets[idx].Description = metadata.Description
assets[idx].Collection.ImageURL = metadata.CollectionImageURL
assets[idx].ImageURL = metadata.ImageURL
} }
} }
} }
// The NFT description field could be arbitrarily large, causing memory management issues upstream. o.setCacheCollectibleData(assets[idx])
// 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]
} }
return nil return nil
} }
func (o *Manager) getIDsNotInCache(chainID uint64, uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleUniqueID { func (o *Manager) isIDInCollectiblesDataCache(id thirdparty.CollectibleUniqueID) bool {
o.nftCacheLock.RLock() o.nftCacheLock.RLock()
defer o.nftCacheLock.RUnlock() defer o.nftCacheLock.RUnlock()
if _, ok := o.nftCache[id.ChainID]; ok {
idsToFetch := make([]thirdparty.CollectibleUniqueID, 0, len(uniqueIDs)) if _, ok := o.nftCache[id.ChainID][id.HashKey()]; ok {
if _, ok := o.nftCache[chainID]; !ok { return true
idsToFetch = uniqueIDs
} else {
for _, id := range uniqueIDs {
if _, ok := o.nftCache[chainID][id.HashKey()]; !ok {
idsToFetch = append(idsToFetch, id)
}
} }
} }
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 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() o.nftCacheLock.RLock()
defer o.nftCacheLock.RUnlock() defer o.nftCacheLock.RUnlock()
assets := make([]opensea.Asset, 0, len(uniqueIDs)) assets := make([]thirdparty.CollectibleData, 0, len(uniqueIDs))
for _, id := range uniqueIDs {
if _, ok := o.nftCache[chainID]; ok { if _, ok := o.nftCache[id.ChainID]; ok {
for _, id := range uniqueIDs { if asset, ok := o.nftCache[id.ChainID][id.HashKey()]; ok {
if asset, ok := o.nftCache[chainID][id.HashKey()]; ok {
assets = append(assets, asset) assets = append(assets, asset)
continue
} }
} }
emptyAsset := thirdparty.CollectibleData{
ID: id,
}
assets = append(assets, emptyAsset)
} }
return assets 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
}

View File

@ -3,6 +3,7 @@ package common
type ChainID uint64 type ChainID uint64
const ( const (
UnknownChainID uint64 = 0
EthereumMainnet uint64 = 1 EthereumMainnet uint64 = 1
EthereumGoerli uint64 = 5 EthereumGoerli uint64 = 5
EthereumSepolia uint64 = 11155111 EthereumSepolia uint64 = 11155111

View File

@ -29,6 +29,7 @@ import (
"github.com/status-im/status-go/services/wallet/thirdparty/coingecko" "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/cryptocompare"
"github.com/status-im/status-go/services/wallet/thirdparty/infura" "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/token"
"github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletevent" "github.com/status-im/status-go/services/wallet/walletevent"
@ -106,7 +107,8 @@ func NewService(
alchemyClient := alchemy.NewClient(config.WalletConfig.AlchemyAPIKeys) alchemyClient := alchemy.NewClient(config.WalletConfig.AlchemyAPIKeys)
infuraClient := infura.NewClient(config.WalletConfig.InfuraAPIKey, config.WalletConfig.InfuraAPIKeySecret) 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{ return &Service{
db: db, db: db,
accountsDB: accountsDB, accountsDB: accountsDB,

View File

@ -15,8 +15,8 @@ import (
"github.com/status-im/status-go/services/wallet/thirdparty" "github.com/status-im/status-go/services/wallet/thirdparty"
) )
func getBaseURL(chainID uint64) (string, error) { func getBaseURL(chainID walletCommon.ChainID) (string, error) {
switch chainID { switch uint64(chainID) {
case walletCommon.EthereumMainnet: case walletCommon.EthereumMainnet:
return "https://eth-mainnet.g.alchemy.com", nil return "https://eth-mainnet.g.alchemy.com", nil
case walletCommon.EthereumGoerli: case walletCommon.EthereumGoerli:
@ -43,7 +43,7 @@ func getAPIKeySubpath(apiKey string) string {
return apiKey return apiKey
} }
func getNFTBaseURL(chainID uint64, apiKey string) (string, error) { func getNFTBaseURL(chainID walletCommon.ChainID, apiKey string) (string, error) {
baseURL, err := getBaseURL(chainID) baseURL, err := getBaseURL(chainID)
if err != nil { if err != nil {
@ -93,7 +93,7 @@ func (o *Client) doQuery(url string) (*http.Response, error) {
return resp, nil return resp, nil
} }
func (o *Client) IsChainSupported(chainID uint64) bool { func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
_, err := getBaseURL(chainID) _, err := getBaseURL(chainID)
return err == nil return err == nil
} }
@ -125,13 +125,13 @@ func alchemyOwnershipToCommon(contractAddress common.Address, alchemyOwnership C
return &ownership, nil 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{ queryParams := url.Values{
"contractAddress": {contractAddress.String()}, "contractAddress": {contractAddress.String()},
"withTokenBalances": {"true"}, "withTokenBalances": {"true"},
} }
url, err := getNFTBaseURL(chainID, o.apiKeys[chainID]) url, err := getNFTBaseURL(chainID, o.apiKeys[uint64(chainID)])
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,29 +1,115 @@
package thirdparty package thirdparty
import ( import (
"fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/status-im/status-go/services/wallet/bigint" "github.com/status-im/status-go/services/wallet/bigint"
w_common "github.com/status-im/status-go/services/wallet/common"
) )
type CollectibleUniqueID struct { type CollectibleUniqueID struct {
ContractAddress common.Address `json:"contractAddress"` ChainID w_common.ChainID `json:"chainID"`
TokenID *bigint.BigInt `json:"tokenID"` ContractAddress common.Address `json:"contractAddress"`
TokenID *bigint.BigInt `json:"tokenID"`
} }
func (k *CollectibleUniqueID) HashKey() string { 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 { func GroupCollectibleUIDsByChainID(uids []CollectibleUniqueID) map[w_common.ChainID][]CollectibleUniqueID {
Name string `json:"name"` ret := make(map[w_common.ChainID][]CollectibleUniqueID)
Description string `json:"description"`
CollectionImageURL string `json:"collection_image"` for _, uid := range uids {
ImageURL string `json:"image"` 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 { type CollectibleMetadataProvider interface {
CanProvideCollectibleMetadata(chainID uint64, id CollectibleUniqueID, tokenURI string) (bool, error) CanProvideCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (bool, error)
FetchCollectibleMetadata(chainID uint64, id CollectibleUniqueID, tokenURI string) (*CollectibleMetadata, error) FetchCollectibleMetadata(id CollectibleUniqueID, tokenURI string) (*CollectibleData, error)
} }
type TokenBalance struct { type TokenBalance struct {
@ -44,6 +130,6 @@ type CollectibleContractOwnership struct {
} }
type CollectibleContractOwnershipProvider interface { type CollectibleContractOwnershipProvider interface {
FetchCollectibleOwnersByContractAddress(chainID uint64, contractAddress common.Address) (*CollectibleContractOwnership, error) FetchCollectibleOwnersByContractAddress(chainID w_common.ChainID, contractAddress common.Address) (*CollectibleContractOwnership, error)
IsChainSupported(chainID uint64) bool IsChainSupported(chainID w_common.ChainID) bool
} }

View File

@ -63,8 +63,8 @@ func (o *Client) doQuery(url string) (*http.Response, error) {
return resp, nil return resp, nil
} }
func (o *Client) IsChainSupported(chainID uint64) bool { func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
switch chainID { switch uint64(chainID) {
case walletCommon.EthereumMainnet, walletCommon.EthereumGoerli, walletCommon.EthereumSepolia, walletCommon.ArbitrumMainnet: case walletCommon.EthereumMainnet, walletCommon.EthereumGoerli, walletCommon.EthereumSepolia, walletCommon.ArbitrumMainnet:
return true return true
} }
@ -98,7 +98,7 @@ func infuraOwnershipToCommon(contractAddress common.Address, ownersMap map[commo
return &ownership, nil 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 := "" cursor := ""
ownersMap := make(map[common.Address][]CollectibleOwner) ownersMap := make(map[common.Address][]CollectibleOwner)

View File

@ -16,6 +16,9 @@ import (
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "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" "github.com/status-im/status-go/services/wallet/bigint"
walletCommon "github.com/status-im/status-go/services/wallet/common" 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/connection"
@ -36,12 +39,17 @@ const GetRequestWaitTime = 300 * time.Millisecond
const ChainIDRequiringAPIKey = walletCommon.EthereumMainnet const ChainIDRequiringAPIKey = walletCommon.EthereumMainnet
const FetchNoLimit = 0
var ( var (
ErrChainIDNotSupported = errors.New("chainID not supported by opensea API") ErrChainIDNotSupported = errors.New("chainID not supported by opensea API")
) )
func getbaseURL(chainID uint64) (string, error) { type urlGetter func(walletCommon.ChainID, string) (string, error)
switch chainID {
func getbaseURL(chainID walletCommon.ChainID) (string, error) {
// v1 Endpoints only support L1 chain
switch uint64(chainID) {
case walletCommon.EthereumMainnet: case walletCommon.EthereumMainnet:
return "https://api.opensea.io/api/v1", nil return "https://api.opensea.io/api/v1", nil
case walletCommon.EthereumGoerli: case walletCommon.EthereumGoerli:
@ -51,6 +59,34 @@ func getbaseURL(chainID uint64) (string, error) {
return "", ErrChainIDNotSupported 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 type TraitValue string
func (st *TraitValue) UnmarshalJSON(b []byte) error { func (st *TraitValue) UnmarshalJSON(b []byte) error {
@ -78,7 +114,8 @@ type AssetContainer struct {
} }
type Contract struct { type Contract struct {
Address string `json:"address"` Address string `json:"address"`
ChainIdentifier string `json:"chain_identifier"`
} }
type Trait struct { type Trait struct {
@ -143,6 +180,62 @@ type OwnedCollection struct {
OwnedAssetCount *bigint.BigInt `json:"owned_asset_count"` 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 { type HTTPClient struct {
client *http.Client client *http.Client
getRequestLock sync.RWMutex getRequestLock sync.RWMutex
@ -242,6 +335,7 @@ type Client struct {
client *HTTPClient client *HTTPClient
apiKey string apiKey string
connectionStatus *connection.Status connectionStatus *connection.Status
urlGetter urlGetter
} }
// new opensea client. // new opensea client.
@ -250,20 +344,21 @@ func NewClient(apiKey string, feed *event.Feed) *Client {
client: newHTTPClient(), client: newHTTPClient(),
apiKey: apiKey, apiKey: apiKey,
connectionStatus: connection.NewStatus(EventCollectibleStatusChanged, feed), 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 offset := 0
var collections []OwnedCollection var collections []OwnedCollection
baseURL, err := getbaseURL(chainID)
if err != nil {
return nil, err
}
for { 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) body, err := o.client.doGetRequest(url, o.apiKey)
if err != nil { if err != nil {
o.connectionStatus.SetIsConnected(false) o.connectionStatus.SetIsConnected(false)
@ -291,7 +386,7 @@ func (o *Client) FetchAllCollectionsByOwner(chainID uint64, owner common.Address
return collections, nil 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{ queryParams := url.Values{
"owner": {owner.String()}, "owner": {owner.String()},
"collection": {collectionSlug}, "collection": {collectionSlug},
@ -304,7 +399,7 @@ func (o *Client) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner common
return o.fetchAssets(chainID, queryParams, limit) 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{ queryParams := url.Values{
"owner": {owner.String()}, "owner": {owner.String()},
} }
@ -320,7 +415,7 @@ func (o *Client) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner c
return o.fetchAssets(chainID, queryParams, limit) 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{ queryParams := url.Values{
"owner": {owner.String()}, "owner": {owner.String()},
} }
@ -332,18 +427,107 @@ func (o *Client) FetchAllAssetsByOwner(chainID uint64, owner common.Address, cur
return o.fetchAssets(chainID, queryParams, limit) 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{} queryParams := url.Values{}
for _, uniqueID := range uniqueIDs { ret := make([]thirdparty.CollectibleData, 0, len(uniqueIDs))
queryParams.Add("token_ids", uniqueID.TokenID.String())
queryParams.Add("asset_contract_addresses", uniqueID.ContractAddress.String()) 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) assets := new(AssetContainer)
if len(queryParams["cursor"]) > 0 { if len(queryParams["cursor"]) > 0 {

View File

@ -9,6 +9,9 @@ import (
"testing" "testing"
"github.com/status-im/status-go/services/wallet/bigint" "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" "github.com/stretchr/testify/assert"
@ -20,8 +23,27 @@ const (
ExpectedExpiredKeyError = "invalid json: Expired API key" 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) { func TestFetchAllCollectionsByOwner(t *testing.T) {
expected := []OwnedCollection{{ expectedOS := []OwnedCollection{{
Collection: Collection{ Collection: Collection{
Name: "Rocky", Name: "Rocky",
Slug: "rocky", Slug: "rocky",
@ -29,7 +51,7 @@ func TestFetchAllCollectionsByOwner(t *testing.T) {
}, },
OwnedAssetCount: &bigint.BigInt{Int: big.NewInt(1)}, 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) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
_, err := w.Write(response) _, err := w.Write(response)
@ -39,15 +61,9 @@ func TestFetchAllCollectionsByOwner(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := &HTTPClient{ opensea := initTestClient(srv)
client: srv.Client(), res, err := opensea.FetchAllCollectionsByOwner(walletCommon.ChainID(1), common.Address{1})
} assert.Equal(t, expectedOS, res)
opensea := &Client{
client: client,
url: srv.URL,
}
res, err := opensea.FetchAllCollectionsByOwner(common.Address{1})
assert.Equal(t, expected, res)
assert.Nil(t, err) assert.Nil(t, err)
} }
@ -61,34 +77,56 @@ func TestFetchAllCollectionsByOwnerWithInValidJson(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := &HTTPClient{ opensea := initTestClient(srv)
client: srv.Client(), res, err := opensea.FetchAllCollectionsByOwner(walletCommon.ChainID(1), common.Address{1})
}
opensea := &Client{
client: client,
url: srv.URL,
}
res, err := opensea.FetchAllCollectionsByOwner(common.Address{1})
assert.Nil(t, res) assert.Nil(t, res)
assert.Equal(t, err, fmt.Errorf(ExpectedExpiredKeyError)) assert.Equal(t, err, fmt.Errorf(ExpectedExpiredKeyError))
} }
func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) { func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) {
expected := AssetContainer{ expectedOS := AssetContainer{
Assets: []Asset{{ Assets: []Asset{{
ID: 1, ID: 1,
TokenID: &bigint.BigInt{Int: big.NewInt(1)},
Name: "Rocky", Name: "Rocky",
Description: "Rocky Balboa", Description: "Rocky Balboa",
Permalink: "permalink", Permalink: "permalink",
ImageThumbnailURL: "ImageThumbnailURL", ImageThumbnailURL: "ImageThumbnailURL",
ImageURL: "ImageUrl", ImageURL: "ImageUrl",
Contract: Contract{Address: "1"}, Contract: Contract{
Collection: Collection{Name: "Rocky"}, Address: "1",
ChainIdentifier: "ethereum",
},
Collection: Collection{
Name: "Rocky",
Traits: map[string]CollectionTrait{},
},
Traits: []Trait{},
}}, }},
NextCursor: "", NextCursor: "",
PreviousCursor: "", 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) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
_, err := w.Write(response) _, err := w.Write(response)
@ -98,16 +136,10 @@ func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := &HTTPClient{ opensea := initTestClient(srv)
client: srv.Client(), res, err := opensea.FetchAllAssetsByOwnerAndCollection(walletCommon.ChainID(1), common.Address{1}, "rocky", "", 200)
}
opensea := &Client{
client: client,
url: srv.URL,
}
res, err := opensea.FetchAllAssetsByOwnerAndCollection(common.Address{1}, "rocky", "", 200)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, expected, *res) assert.Equal(t, expectedCommon, *res)
} }
func TestFetchAllAssetsByOwnerAndCollectionInvalidJson(t *testing.T) { func TestFetchAllAssetsByOwnerAndCollectionInvalidJson(t *testing.T) {
@ -120,14 +152,8 @@ func TestFetchAllAssetsByOwnerAndCollectionInvalidJson(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
client := &HTTPClient{ opensea := initTestClient(srv)
client: srv.Client(), res, err := opensea.FetchAllAssetsByOwnerAndCollection(walletCommon.ChainID(1), common.Address{1}, "rocky", "", 200)
}
opensea := &Client{
client: client,
url: srv.URL,
}
res, err := opensea.FetchAllAssetsByOwnerAndCollection(common.Address{1}, "rocky", "", 200)
assert.Nil(t, res) assert.Nil(t, res)
assert.Equal(t, fmt.Errorf(ExpectedExpiredKeyError), err) assert.Equal(t, fmt.Errorf(ExpectedExpiredKeyError), err)
} }