2023-03-21 13:52:14 +00:00
|
|
|
package collectibles
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-06-28 10:48:33 +00:00
|
|
|
"fmt"
|
2023-07-13 17:26:17 +00:00
|
|
|
"math/big"
|
2023-03-21 13:52:14 +00:00
|
|
|
"strings"
|
2023-03-30 21:01:28 +00:00
|
|
|
"sync"
|
2023-03-21 13:52:14 +00:00
|
|
|
"time"
|
|
|
|
|
2023-04-17 11:42:01 +00:00
|
|
|
"github.com/afex/hystrix-go/hystrix"
|
|
|
|
|
2023-03-21 13:52:14 +00:00
|
|
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
"github.com/status-im/status-go/contracts/collectibles"
|
|
|
|
"github.com/status-im/status-go/rpc"
|
2023-07-13 17:26:17 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/bigint"
|
2023-07-18 15:01:53 +00:00
|
|
|
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
2023-03-21 13:52:14 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/thirdparty"
|
|
|
|
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
|
|
|
|
)
|
|
|
|
|
|
|
|
const requestTimeout = 5 * time.Second
|
|
|
|
|
2023-04-17 11:42:01 +00:00
|
|
|
const hystrixContractOwnershipClientName = "contractOwnershipClient"
|
|
|
|
|
2023-06-06 17:49:36 +00:00
|
|
|
// ERC721 does not support function "TokenURI" if call
|
|
|
|
// returns error starting with one of these strings
|
|
|
|
var noTokenURIErrorPrefixes = []string{
|
|
|
|
"execution reverted",
|
|
|
|
"abi: attempting to unmarshall",
|
|
|
|
}
|
|
|
|
|
2023-03-21 13:52:14 +00:00
|
|
|
type Manager struct {
|
2023-04-17 11:42:01 +00:00
|
|
|
rpcClient *rpc.Client
|
2023-07-05 09:33:48 +00:00
|
|
|
mainContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider
|
|
|
|
fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider
|
|
|
|
metadataProvider thirdparty.CollectibleMetadataProvider
|
2023-07-10 09:02:17 +00:00
|
|
|
opensea *opensea.Client
|
2023-07-18 15:01:53 +00:00
|
|
|
nftCache map[walletCommon.ChainID]map[string]thirdparty.CollectibleData
|
2023-04-17 11:42:01 +00:00
|
|
|
nftCacheLock sync.RWMutex
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func NewManager(rpcClient *rpc.Client, mainContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, fallbackContractOwnershipProvider thirdparty.CollectibleContractOwnershipProvider, opensea *opensea.Client) *Manager {
|
2023-04-17 11:42:01 +00:00
|
|
|
hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{
|
|
|
|
Timeout: 10000,
|
|
|
|
MaxConcurrentRequests: 100,
|
|
|
|
SleepWindow: 300000,
|
|
|
|
ErrorPercentThreshold: 25,
|
|
|
|
})
|
|
|
|
|
2023-03-21 13:52:14 +00:00
|
|
|
return &Manager{
|
2023-04-17 11:42:01 +00:00
|
|
|
rpcClient: rpcClient,
|
|
|
|
mainContractOwnershipProvider: mainContractOwnershipProvider,
|
|
|
|
fallbackContractOwnershipProvider: fallbackContractOwnershipProvider,
|
2023-07-18 15:01:53 +00:00
|
|
|
opensea: opensea,
|
2023-04-17 11:42:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeContractOwnershipCall(main func() (any, error), fallback func() (any, error)) (any, error) {
|
|
|
|
resultChan := make(chan any, 1)
|
|
|
|
errChan := hystrix.Go(hystrixContractOwnershipClientName, func() error {
|
|
|
|
res, err := main()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resultChan <- res
|
|
|
|
return nil
|
|
|
|
}, func(err error) error {
|
|
|
|
if fallback == nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := fallback()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resultChan <- res
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
select {
|
|
|
|
case result := <-resultChan:
|
|
|
|
return result, nil
|
|
|
|
case err := <-errChan:
|
|
|
|
return nil, err
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-12 18:27:36 +00:00
|
|
|
// Used to break circular dependency, call once as soon as possible after initialization
|
2023-07-05 09:33:48 +00:00
|
|
|
func (o *Manager) SetMetadataProvider(metadataProvider thirdparty.CollectibleMetadataProvider) {
|
2023-07-12 18:27:36 +00:00
|
|
|
o.metadataProvider = metadataProvider
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) FetchAllCollectionsByOwner(chainID walletCommon.ChainID, owner common.Address) ([]opensea.OwnedCollection, error) {
|
2023-07-10 09:02:17 +00:00
|
|
|
return o.opensea.FetchAllCollectionsByOwner(chainID, owner)
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
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) {
|
2023-07-10 09:02:17 +00:00
|
|
|
assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndCollection(chainID, owner, collectionSlug, cursor, limit)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
err = o.processAssets(assetContainer.Collectibles)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return assetContainer, nil
|
|
|
|
}
|
|
|
|
|
2023-07-13 17:26:17 +00:00
|
|
|
// Need to combine different providers to support all needed ChainIDs
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID walletCommon.ChainID, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
|
2023-07-13 17:26:17 +00:00
|
|
|
ret := make(thirdparty.TokenBalancesPerContractAddress)
|
|
|
|
|
|
|
|
for _, contractAddress := range contractAddresses {
|
|
|
|
ret[contractAddress] = make([]thirdparty.TokenBalance, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try with more direct endpoint first (OpenSea)
|
|
|
|
assetsContainer, err := o.FetchAllAssetsByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses, "", 0)
|
|
|
|
if err == opensea.ErrChainIDNotSupported {
|
|
|
|
// Use contract ownership providers
|
|
|
|
for _, contractAddress := range contractAddresses {
|
2023-07-05 09:33:48 +00:00
|
|
|
ownership, err := o.FetchCollectibleOwnersByContractAddress(chainID, contractAddress)
|
2023-07-13 17:26:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
for _, nftOwner := range ownership.Owners {
|
|
|
|
if nftOwner.OwnerAddress == ownerAddress {
|
|
|
|
ret[contractAddress] = nftOwner.TokenBalances
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if err == nil {
|
|
|
|
// OpenSea could provide
|
2023-07-18 15:01:53 +00:00
|
|
|
for _, collectible := range assetsContainer.Collectibles {
|
|
|
|
contractAddress := collectible.ID.ContractAddress
|
2023-07-13 17:26:17 +00:00
|
|
|
balance := thirdparty.TokenBalance{
|
2023-07-18 15:01:53 +00:00
|
|
|
TokenID: collectible.ID.TokenID,
|
2023-07-13 17:26:17 +00:00
|
|
|
Balance: &bigint.BigInt{Int: big.NewInt(1)},
|
|
|
|
}
|
|
|
|
ret[contractAddress] = append(ret[contractAddress], balance)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// OpenSea could have provided, but returned error
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
|
2023-07-10 09:02:17 +00:00
|
|
|
assetContainer, err := o.opensea.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
err = o.processAssets(assetContainer.Collectibles)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return assetContainer, nil
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleDataContainer, error) {
|
2023-07-10 09:02:17 +00:00
|
|
|
assetContainer, err := o.opensea.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
err = o.processAssets(assetContainer.Collectibles)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return assetContainer, nil
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleData, error) {
|
|
|
|
idsToFetch := o.getIDsNotInCollectiblesDataCache(uniqueIDs)
|
2023-03-30 21:01:28 +00:00
|
|
|
if len(idsToFetch) > 0 {
|
2023-07-18 15:01:53 +00:00
|
|
|
fetchedAssets, err := o.opensea.FetchAssetsByCollectibleUniqueID(idsToFetch)
|
2023-03-30 21:01:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
err = o.processAssets(fetchedAssets)
|
2023-03-30 21:01:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
return o.getCacheCollectiblesData(uniqueIDs), nil
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
|
2023-04-17 11:42:01 +00:00
|
|
|
mainFunc := func() (any, error) {
|
2023-07-05 09:33:48 +00:00
|
|
|
return o.mainContractOwnershipProvider.FetchCollectibleOwnersByContractAddress(chainID, contractAddress)
|
2023-04-17 11:42:01 +00:00
|
|
|
}
|
|
|
|
var fallbackFunc func() (any, error) = nil
|
|
|
|
if o.fallbackContractOwnershipProvider != nil && o.fallbackContractOwnershipProvider.IsChainSupported(chainID) {
|
|
|
|
fallbackFunc = func() (any, error) {
|
2023-07-05 09:33:48 +00:00
|
|
|
return o.fallbackContractOwnershipProvider.FetchCollectibleOwnersByContractAddress(chainID, contractAddress)
|
2023-04-17 11:42:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
owners, err := makeContractOwnershipCall(mainFunc, fallbackFunc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-05 09:33:48 +00:00
|
|
|
return owners.(*thirdparty.CollectibleContractOwnership), nil
|
2023-04-17 11:42:01 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func isMetadataEmpty(asset thirdparty.CollectibleData) bool {
|
2023-03-21 13:52:14 +00:00
|
|
|
return asset.Name == "" &&
|
|
|
|
asset.Description == "" &&
|
|
|
|
asset.ImageURL == "" &&
|
|
|
|
asset.TokenURI == ""
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) fetchTokenURI(id thirdparty.CollectibleUniqueID) (string, error) {
|
|
|
|
backend, err := o.rpcClient.EthClient(uint64(id.ChainID))
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
2023-03-29 16:36:23 +00:00
|
|
|
return "", err
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
caller, err := collectibles.NewCollectiblesCaller(id.ContractAddress, backend)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
2023-03-29 16:36:23 +00:00
|
|
|
return "", err
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
timeoutContext, timeoutCancel := context.WithTimeout(context.Background(), requestTimeout)
|
|
|
|
defer timeoutCancel()
|
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
tokenURI, err := caller.TokenURI(&bind.CallOpts{
|
2023-03-21 13:52:14 +00:00
|
|
|
Context: timeoutContext,
|
2023-03-29 16:36:23 +00:00
|
|
|
}, id.TokenID.Int)
|
2023-03-21 13:52:14 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2023-06-06 17:49:36 +00:00
|
|
|
for _, errorPrefix := range noTokenURIErrorPrefixes {
|
|
|
|
if strings.HasPrefix(err.Error(), errorPrefix) {
|
|
|
|
// Contract doesn't support "TokenURI" method
|
|
|
|
return "", nil
|
|
|
|
}
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
return tokenURI, err
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) processAssets(assets []thirdparty.CollectibleData) error {
|
2023-03-21 13:52:14 +00:00
|
|
|
for idx, asset := range assets {
|
2023-07-18 15:01:53 +00:00
|
|
|
id := asset.ID
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-03-30 21:01:28 +00:00
|
|
|
if isMetadataEmpty(asset) {
|
2023-06-28 10:48:33 +00:00
|
|
|
if o.metadataProvider == nil {
|
2023-07-05 09:33:48 +00:00
|
|
|
return fmt.Errorf("CollectibleMetadataProvider not available")
|
2023-06-28 10:48:33 +00:00
|
|
|
}
|
2023-07-18 15:01:53 +00:00
|
|
|
tokenURI, err := o.fetchTokenURI(id)
|
2023-03-21 13:52:14 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
assets[idx].TokenURI = tokenURI
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
canProvide, err := o.metadataProvider.CanProvideCollectibleMetadata(id, tokenURI)
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
if canProvide {
|
2023-07-18 15:01:53 +00:00
|
|
|
metadata, err := o.metadataProvider.FetchCollectibleMetadata(id, tokenURI)
|
2023-03-21 13:52:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-03-29 16:36:23 +00:00
|
|
|
if metadata != nil {
|
2023-07-18 15:01:53 +00:00
|
|
|
assets[idx] = *metadata
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-03-30 21:01:28 +00:00
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
o.setCacheCollectibleData(assets[idx])
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-03-30 21:01:28 +00:00
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) isIDInCollectiblesDataCache(id thirdparty.CollectibleUniqueID) bool {
|
2023-03-30 21:01:28 +00:00
|
|
|
o.nftCacheLock.RLock()
|
|
|
|
defer o.nftCacheLock.RUnlock()
|
2023-07-18 15:01:53 +00:00
|
|
|
if _, ok := o.nftCache[id.ChainID]; ok {
|
|
|
|
if _, ok := o.nftCache[id.ChainID][id.HashKey()]; ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2023-03-30 21:01:28 +00:00
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Manager) getIDsNotInCollectiblesDataCache(uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleUniqueID {
|
2023-07-05 09:33:48 +00:00
|
|
|
idsToFetch := make([]thirdparty.CollectibleUniqueID, 0, len(uniqueIDs))
|
2023-07-18 15:01:53 +00:00
|
|
|
for _, id := range uniqueIDs {
|
|
|
|
if o.isIDInCollectiblesDataCache(id) {
|
|
|
|
continue
|
2023-03-30 21:01:28 +00:00
|
|
|
}
|
2023-07-18 15:01:53 +00:00
|
|
|
idsToFetch = append(idsToFetch, id)
|
2023-03-30 21:01:28 +00:00
|
|
|
}
|
|
|
|
return idsToFetch
|
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) getCacheCollectiblesData(uniqueIDs []thirdparty.CollectibleUniqueID) []thirdparty.CollectibleData {
|
2023-03-30 21:01:28 +00:00
|
|
|
o.nftCacheLock.RLock()
|
|
|
|
defer o.nftCacheLock.RUnlock()
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
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 {
|
2023-03-30 21:01:28 +00:00
|
|
|
assets = append(assets, asset)
|
2023-07-18 15:01:53 +00:00
|
|
|
continue
|
2023-03-30 21:01:28 +00:00
|
|
|
}
|
|
|
|
}
|
2023-07-18 15:01:53 +00:00
|
|
|
emptyAsset := thirdparty.CollectibleData{
|
|
|
|
ID: id,
|
|
|
|
}
|
|
|
|
assets = append(assets, emptyAsset)
|
2023-03-30 21:01:28 +00:00
|
|
|
}
|
|
|
|
return assets
|
|
|
|
}
|
2023-07-18 15:01:53 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|