2023-03-21 13:52:14 +00:00
|
|
|
package collectibles
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-08-07 22:30:18 +00:00
|
|
|
"database/sql"
|
2023-08-01 23:17:59 +00:00
|
|
|
"errors"
|
2023-06-28 10:48:33 +00:00
|
|
|
"fmt"
|
2023-07-13 17:26:17 +00:00
|
|
|
"math/big"
|
2023-07-31 23:34:53 +00:00
|
|
|
"net/http"
|
2023-03-21 13:52:14 +00:00
|
|
|
"strings"
|
|
|
|
"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"
|
2023-09-22 13:18:42 +00:00
|
|
|
"github.com/ethereum/go-ethereum/event"
|
2023-07-31 23:34:53 +00:00
|
|
|
"github.com/ethereum/go-ethereum/log"
|
2023-07-18 08:33:45 +00:00
|
|
|
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
|
2023-03-21 13:52:14 +00:00
|
|
|
"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-09-22 13:18:42 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/connection"
|
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"
|
2023-09-22 13:18:42 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/walletevent"
|
2023-03-21 13:52:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const requestTimeout = 5 * time.Second
|
|
|
|
|
2023-04-17 11:42:01 +00:00
|
|
|
const hystrixContractOwnershipClientName = "contractOwnershipClient"
|
|
|
|
|
2023-09-22 13:18:42 +00:00
|
|
|
const EventCollectiblesConnectionStatusChanged walletevent.EventType = "wallet-collectible-status-changed"
|
|
|
|
|
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-08-01 23:17:59 +00:00
|
|
|
var (
|
2023-09-22 13:18:42 +00:00
|
|
|
ErrAllProvidersFailedForChainID = errors.New("all providers failed for chainID")
|
2023-08-01 23:17:59 +00:00
|
|
|
ErrNoProvidersAvailableForChainID = errors.New("no providers available for chainID")
|
|
|
|
)
|
|
|
|
|
2023-08-11 17:28:46 +00:00
|
|
|
type ManagerInterface interface {
|
|
|
|
FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error)
|
|
|
|
}
|
|
|
|
|
2023-03-21 13:52:14 +00:00
|
|
|
type Manager struct {
|
2023-08-01 23:17:59 +00:00
|
|
|
rpcClient *rpc.Client
|
|
|
|
contractOwnershipProviders []thirdparty.CollectibleContractOwnershipProvider
|
|
|
|
accountOwnershipProviders []thirdparty.CollectibleAccountOwnershipProvider
|
2023-08-03 12:24:23 +00:00
|
|
|
collectibleDataProviders []thirdparty.CollectibleDataProvider
|
2023-08-16 13:01:57 +00:00
|
|
|
collectionDataProviders []thirdparty.CollectionDataProvider
|
2023-09-22 13:18:42 +00:00
|
|
|
collectibleProviders []thirdparty.CollectibleProvider
|
2023-08-01 23:17:59 +00:00
|
|
|
metadataProvider thirdparty.CollectibleMetadataProvider
|
2023-09-21 12:40:58 +00:00
|
|
|
communityInfoProvider thirdparty.CollectibleCommunityInfoProvider
|
2023-09-22 13:18:42 +00:00
|
|
|
|
|
|
|
opensea *opensea.Client
|
|
|
|
httpClient *http.Client
|
|
|
|
|
|
|
|
collectiblesDataDB *CollectibleDataDB
|
|
|
|
collectionsDataDB *CollectionDataDB
|
|
|
|
|
|
|
|
statuses map[string]*connection.Status
|
|
|
|
statusNotifier *connection.StatusNotifier
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-09-21 12:40:58 +00:00
|
|
|
func NewManager(
|
|
|
|
db *sql.DB,
|
|
|
|
rpcClient *rpc.Client,
|
|
|
|
contractOwnershipProviders []thirdparty.CollectibleContractOwnershipProvider,
|
|
|
|
accountOwnershipProviders []thirdparty.CollectibleAccountOwnershipProvider,
|
|
|
|
collectibleDataProviders []thirdparty.CollectibleDataProvider,
|
|
|
|
collectionDataProviders []thirdparty.CollectionDataProvider,
|
2023-09-22 13:18:42 +00:00
|
|
|
opensea *opensea.Client,
|
|
|
|
feed *event.Feed) *Manager {
|
2023-04-17 11:42:01 +00:00
|
|
|
hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{
|
|
|
|
Timeout: 10000,
|
|
|
|
MaxConcurrentRequests: 100,
|
|
|
|
SleepWindow: 300000,
|
|
|
|
ErrorPercentThreshold: 25,
|
|
|
|
})
|
|
|
|
|
2023-09-22 13:18:42 +00:00
|
|
|
ownershipDB := NewOwnershipDB(db)
|
|
|
|
|
|
|
|
statuses := make(map[string]*connection.Status)
|
|
|
|
|
|
|
|
allChainIDs := walletCommon.AllChainIDs()
|
|
|
|
for _, chainID := range allChainIDs {
|
|
|
|
status := connection.NewStatus()
|
|
|
|
state := status.GetState()
|
|
|
|
latestUpdateTimestamp, err := ownershipDB.GetLatestOwnershipUpdateTimestamp(chainID)
|
|
|
|
if err == nil {
|
|
|
|
state.LastSuccessAt = latestUpdateTimestamp
|
|
|
|
status.SetState(state)
|
|
|
|
}
|
|
|
|
statuses[chainID.String()] = status
|
|
|
|
}
|
|
|
|
|
|
|
|
statusNotifier := connection.NewStatusNotifier(
|
|
|
|
statuses,
|
|
|
|
EventCollectiblesConnectionStatusChanged,
|
|
|
|
feed,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Get list of all providers
|
|
|
|
collectibleProvidersMap := make(map[string]thirdparty.CollectibleProvider)
|
|
|
|
collectibleProviders := make([]thirdparty.CollectibleProvider, 0)
|
|
|
|
for _, provider := range contractOwnershipProviders {
|
|
|
|
collectibleProvidersMap[provider.ID()] = provider
|
|
|
|
}
|
|
|
|
for _, provider := range accountOwnershipProviders {
|
|
|
|
collectibleProvidersMap[provider.ID()] = provider
|
|
|
|
}
|
|
|
|
for _, provider := range collectibleDataProviders {
|
|
|
|
collectibleProvidersMap[provider.ID()] = provider
|
|
|
|
}
|
|
|
|
for _, provider := range collectionDataProviders {
|
|
|
|
collectibleProvidersMap[provider.ID()] = provider
|
|
|
|
}
|
|
|
|
for _, provider := range collectibleProvidersMap {
|
|
|
|
collectibleProviders = append(collectibleProviders, provider)
|
|
|
|
}
|
|
|
|
|
2023-03-21 13:52:14 +00:00
|
|
|
return &Manager{
|
2023-08-01 23:17:59 +00:00
|
|
|
rpcClient: rpcClient,
|
|
|
|
contractOwnershipProviders: contractOwnershipProviders,
|
|
|
|
accountOwnershipProviders: accountOwnershipProviders,
|
2023-08-03 12:24:23 +00:00
|
|
|
collectibleDataProviders: collectibleDataProviders,
|
2023-08-16 13:01:57 +00:00
|
|
|
collectionDataProviders: collectionDataProviders,
|
2023-09-22 13:18:42 +00:00
|
|
|
collectibleProviders: collectibleProviders,
|
2023-08-01 23:17:59 +00:00
|
|
|
opensea: opensea,
|
2023-07-31 23:34:53 +00:00
|
|
|
httpClient: &http.Client{
|
|
|
|
Timeout: requestTimeout,
|
|
|
|
},
|
2023-08-07 22:30:18 +00:00
|
|
|
collectiblesDataDB: NewCollectibleDataDB(db),
|
|
|
|
collectionsDataDB: NewCollectionDataDB(db),
|
2023-09-22 13:18:42 +00:00
|
|
|
statuses: statuses,
|
|
|
|
statusNotifier: statusNotifier,
|
2023-04-17 11:42:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
func mapToList[K comparable, T any](m map[K]T) []T {
|
2023-08-03 12:24:23 +00:00
|
|
|
list := make([]T, 0, len(m))
|
|
|
|
for _, v := range m {
|
2023-08-07 22:30:18 +00:00
|
|
|
list = append(list, v)
|
2023-08-03 12:24:23 +00:00
|
|
|
}
|
|
|
|
return list
|
|
|
|
}
|
|
|
|
|
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-31 23:34:53 +00:00
|
|
|
func (o *Manager) doContentTypeRequest(url string) (string, error) {
|
|
|
|
req, err := http.NewRequest(http.MethodHead, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := o.httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
if err := resp.Body.Close(); err != nil {
|
|
|
|
log.Error("failed to close head request body", "err", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return resp.Header.Get("Content-Type"), nil
|
|
|
|
}
|
|
|
|
|
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-09-21 12:40:58 +00:00
|
|
|
func (o *Manager) SetCommunityInfoProvider(communityInfoProvider thirdparty.CollectibleCommunityInfoProvider) {
|
|
|
|
o.communityInfoProvider = communityInfoProvider
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID walletCommon.ChainID, owner common.Address, collectionSlug string, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, 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-31 19:41:14 +00:00
|
|
|
err = o.processFullCollectibleData(assetContainer.Items)
|
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)
|
|
|
|
}
|
|
|
|
|
2023-08-01 23:17:59 +00:00
|
|
|
// Try with account ownership providers first
|
2023-07-31 23:34:53 +00:00
|
|
|
assetsContainer, err := o.FetchAllAssetsByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses, thirdparty.FetchFromStartCursor, thirdparty.FetchNoLimit)
|
2023-08-01 23:17:59 +00:00
|
|
|
if err == ErrNoProvidersAvailableForChainID {
|
2023-07-13 17:26:17 +00:00
|
|
|
// 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 {
|
2023-08-01 23:17:59 +00:00
|
|
|
// Account ownership providers succeeded
|
2023-07-31 19:41:14 +00:00
|
|
|
for _, fullData := range assetsContainer.Items {
|
|
|
|
contractAddress := fullData.CollectibleData.ID.ContractID.Address
|
2023-07-13 17:26:17 +00:00
|
|
|
balance := thirdparty.TokenBalance{
|
2023-07-31 19:41:14 +00:00
|
|
|
TokenID: fullData.CollectibleData.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-31 19:41:14 +00:00
|
|
|
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
2023-09-22 13:18:42 +00:00
|
|
|
defer o.checkConnectionStatus(chainID)
|
|
|
|
|
|
|
|
anyProviderAvailable := false
|
2023-08-01 23:17:59 +00:00
|
|
|
for _, provider := range o.accountOwnershipProviders {
|
|
|
|
if !provider.IsChainSupported(chainID) {
|
|
|
|
continue
|
|
|
|
}
|
2023-09-22 13:18:42 +00:00
|
|
|
anyProviderAvailable = true
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-08-01 23:17:59 +00:00
|
|
|
assetContainer, err := provider.FetchAllAssetsByOwnerAndContractAddress(chainID, owner, contractAddresses, cursor, limit)
|
|
|
|
if err != nil {
|
2023-09-22 13:18:42 +00:00
|
|
|
log.Error("FetchAllAssetsByOwnerAndContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
|
|
|
continue
|
2023-08-01 23:17:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = o.processFullCollectibleData(assetContainer.Items)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return assetContainer, nil
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-09-22 13:18:42 +00:00
|
|
|
if anyProviderAvailable {
|
|
|
|
return nil, ErrAllProvidersFailedForChainID
|
|
|
|
}
|
2023-08-01 23:17:59 +00:00
|
|
|
return nil, ErrNoProvidersAvailableForChainID
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Manager) FetchAllAssetsByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
2023-09-22 13:18:42 +00:00
|
|
|
defer o.checkConnectionStatus(chainID)
|
|
|
|
|
|
|
|
anyProviderAvailable := false
|
2023-08-01 23:17:59 +00:00
|
|
|
for _, provider := range o.accountOwnershipProviders {
|
|
|
|
if !provider.IsChainSupported(chainID) {
|
|
|
|
continue
|
|
|
|
}
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-08-01 23:17:59 +00:00
|
|
|
assetContainer, err := provider.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
|
|
|
|
if err != nil {
|
2023-09-22 13:18:42 +00:00
|
|
|
log.Error("FetchAllAssetsByOwner failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
|
|
|
continue
|
2023-08-01 23:17:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = o.processFullCollectibleData(assetContainer.Items)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return assetContainer, nil
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-09-22 13:18:42 +00:00
|
|
|
if anyProviderAvailable {
|
|
|
|
return nil, ErrAllProvidersFailedForChainID
|
|
|
|
}
|
2023-08-01 23:17:59 +00:00
|
|
|
return nil, ErrNoProvidersAvailableForChainID
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-26 17:48:14 +00:00
|
|
|
func (o *Manager) FetchCollectibleOwnershipByOwner(chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.CollectibleOwnershipContainer, error) {
|
2023-07-31 19:41:14 +00:00
|
|
|
// We don't yet have an API that will return only Ownership data
|
|
|
|
// Use the full Ownership + Metadata endpoint and use the data we need
|
2023-07-26 17:48:14 +00:00
|
|
|
assetContainer, err := o.FetchAllAssetsByOwner(chainID, owner, cursor, limit)
|
2023-07-18 15:02:56 +00:00
|
|
|
if err != nil {
|
2023-07-26 17:48:14 +00:00
|
|
|
return nil, err
|
2023-07-18 15:02:56 +00:00
|
|
|
}
|
|
|
|
|
2023-07-26 17:48:14 +00:00
|
|
|
ret := assetContainer.ToOwnershipContainer()
|
|
|
|
return &ret, nil
|
2023-07-18 15:02:56 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
func (o *Manager) FetchAssetsByCollectibleUniqueID(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
2023-08-07 22:30:18 +00:00
|
|
|
missingIDs, err := o.collectiblesDataDB.GetIDsNotInDB(uniqueIDs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-03-30 21:01:28 +00:00
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
missingIDsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(missingIDs)
|
|
|
|
|
|
|
|
for chainID, idsToFetch := range missingIDsPerChainID {
|
2023-09-22 13:18:42 +00:00
|
|
|
defer o.checkConnectionStatus(chainID)
|
|
|
|
|
2023-08-03 12:24:23 +00:00
|
|
|
for _, provider := range o.collectibleDataProviders {
|
|
|
|
if !provider.IsChainSupported(chainID) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-08-23 17:17:15 +00:00
|
|
|
fetchedAssets, err := provider.FetchAssetsByCollectibleUniqueID(idsToFetch)
|
2023-08-03 12:24:23 +00:00
|
|
|
if err != nil {
|
2023-09-22 13:18:42 +00:00
|
|
|
log.Error("FetchAssetsByCollectibleUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
|
|
|
continue
|
2023-08-03 12:24:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = o.processFullCollectibleData(fetchedAssets)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
2023-03-30 21:01:28 +00:00
|
|
|
}
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
return o.getCacheFullCollectibleData(uniqueIDs)
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-08-03 12:24:23 +00:00
|
|
|
func (o *Manager) FetchCollectionsDataByContractID(ids []thirdparty.ContractID) ([]thirdparty.CollectionData, error) {
|
2023-08-07 22:30:18 +00:00
|
|
|
missingIDs, err := o.collectionsDataDB.GetIDsNotInDB(ids)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-03 12:24:23 +00:00
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
missingIDsPerChainID := thirdparty.GroupContractIDsByChainID(missingIDs)
|
|
|
|
|
|
|
|
for chainID, idsToFetch := range missingIDsPerChainID {
|
2023-09-22 13:18:42 +00:00
|
|
|
defer o.checkConnectionStatus(chainID)
|
|
|
|
|
2023-08-16 13:01:57 +00:00
|
|
|
for _, provider := range o.collectionDataProviders {
|
2023-08-03 12:24:23 +00:00
|
|
|
if !provider.IsChainSupported(chainID) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
fetchedCollections, err := provider.FetchCollectionsDataByContractID(idsToFetch)
|
|
|
|
if err != nil {
|
2023-09-22 13:18:42 +00:00
|
|
|
log.Error("FetchCollectionsDataByContractID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
|
|
|
continue
|
2023-08-03 12:24:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = o.processCollectionData(fetchedCollections)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
data, err := o.collectionsDataDB.GetData(ids)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return mapToList(data), nil
|
2023-08-03 12:24:23 +00:00
|
|
|
}
|
|
|
|
|
2023-08-01 23:17:59 +00:00
|
|
|
func (o *Manager) getContractOwnershipProviders(chainID walletCommon.ChainID) (mainProvider thirdparty.CollectibleContractOwnershipProvider, fallbackProvider thirdparty.CollectibleContractOwnershipProvider) {
|
|
|
|
mainProvider = nil
|
|
|
|
fallbackProvider = nil
|
|
|
|
|
|
|
|
for _, provider := range o.contractOwnershipProviders {
|
|
|
|
if provider.IsChainSupported(chainID) {
|
|
|
|
if mainProvider == nil {
|
|
|
|
// First provider found
|
|
|
|
mainProvider = provider
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Second provider found
|
|
|
|
fallbackProvider = provider
|
|
|
|
break
|
2023-04-17 11:42:01 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-01 23:17:59 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCollectibleOwnersByContractAddressFunc(chainID walletCommon.ChainID, contractAddress common.Address, provider thirdparty.CollectibleContractOwnershipProvider) func() (any, error) {
|
|
|
|
if provider == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return func() (any, error) {
|
2023-09-22 13:18:42 +00:00
|
|
|
res, err := provider.FetchCollectibleOwnersByContractAddress(chainID, contractAddress)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("FetchCollectibleOwnersByContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
|
|
|
}
|
|
|
|
return res, err
|
2023-08-01 23:17:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Manager) FetchCollectibleOwnersByContractAddress(chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
|
2023-09-22 13:18:42 +00:00
|
|
|
defer o.checkConnectionStatus(chainID)
|
|
|
|
|
2023-08-01 23:17:59 +00:00
|
|
|
mainProvider, fallbackProvider := o.getContractOwnershipProviders(chainID)
|
|
|
|
if mainProvider == nil {
|
|
|
|
return nil, ErrNoProvidersAvailableForChainID
|
|
|
|
}
|
|
|
|
|
|
|
|
mainFn := getCollectibleOwnersByContractAddressFunc(chainID, contractAddress, mainProvider)
|
|
|
|
fallbackFn := getCollectibleOwnersByContractAddressFunc(chainID, contractAddress, fallbackProvider)
|
|
|
|
|
|
|
|
owners, err := makeContractOwnershipCall(mainFn, fallbackFn)
|
2023-04-17 11:42:01 +00:00
|
|
|
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 == "" &&
|
2023-09-21 12:40:58 +00:00
|
|
|
asset.ImageURL == ""
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 15:01:53 +00:00
|
|
|
func (o *Manager) fetchTokenURI(id thirdparty.CollectibleUniqueID) (string, error) {
|
2023-09-11 09:54:37 +00:00
|
|
|
if id.TokenID == nil {
|
|
|
|
return "", errors.New("empty token ID")
|
|
|
|
}
|
2023-07-31 19:41:14 +00:00
|
|
|
backend, err := o.rpcClient.EthClient(uint64(id.ContractID.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-07-31 19:41:14 +00:00
|
|
|
caller, err := collectibles.NewCollectiblesCaller(id.ContractID.Address, 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-31 19:41:14 +00:00
|
|
|
func (o *Manager) processFullCollectibleData(assets []thirdparty.FullCollectibleData) error {
|
2023-08-07 22:30:18 +00:00
|
|
|
collectiblesData := make([]thirdparty.CollectibleData, 0, len(assets))
|
|
|
|
collectionsData := make([]thirdparty.CollectionData, 0, len(assets))
|
2023-08-03 12:24:23 +00:00
|
|
|
missingCollectionIDs := make([]thirdparty.ContractID, 0)
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
for _, asset := range assets {
|
2023-07-31 19:41:14 +00:00
|
|
|
id := asset.CollectibleData.ID
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-07-31 23:34:53 +00:00
|
|
|
// Get Metadata from alternate source if empty
|
2023-07-31 19:41:14 +00:00
|
|
|
if isMetadataEmpty(asset.CollectibleData) {
|
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-03-21 13:52:14 +00:00
|
|
|
|
2023-09-21 12:40:58 +00:00
|
|
|
tokenURI := asset.CollectibleData.TokenURI
|
|
|
|
var err error
|
2023-03-21 13:52:14 +00:00
|
|
|
|
2023-09-21 12:40:58 +00:00
|
|
|
if tokenURI == "" {
|
|
|
|
tokenURI, err = o.fetchTokenURI(id)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
asset.CollectibleData.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-08-07 22:30:18 +00:00
|
|
|
asset = *metadata
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-03-30 21:01:28 +00:00
|
|
|
|
2023-07-31 23:34:53 +00:00
|
|
|
// Get Animation MediaType
|
2023-08-07 22:30:18 +00:00
|
|
|
if len(asset.CollectibleData.AnimationURL) > 0 {
|
|
|
|
contentType, err := o.doContentTypeRequest(asset.CollectibleData.AnimationURL)
|
2023-07-31 23:34:53 +00:00
|
|
|
if err != nil {
|
2023-08-07 22:30:18 +00:00
|
|
|
asset.CollectibleData.AnimationURL = ""
|
2023-07-31 23:34:53 +00:00
|
|
|
}
|
2023-08-07 22:30:18 +00:00
|
|
|
asset.CollectibleData.AnimationMediaType = contentType
|
2023-07-31 23:34:53 +00:00
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
collectiblesData = append(collectiblesData, asset.CollectibleData)
|
|
|
|
if asset.CollectionData != nil {
|
|
|
|
collectionsData = append(collectionsData, *asset.CollectionData)
|
2023-08-03 12:24:23 +00:00
|
|
|
} else {
|
|
|
|
missingCollectionIDs = append(missingCollectionIDs, id.ContractID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
err := o.collectiblesDataDB.SetData(collectiblesData)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = o.collectionsDataDB.SetData(collectionsData)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-08-03 12:24:23 +00:00
|
|
|
if len(missingCollectionIDs) > 0 {
|
|
|
|
// Calling this ensures collection data is fetched and cached (if not already available)
|
|
|
|
_, err := o.FetchCollectionsDataByContractID(missingCollectionIDs)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2023-07-31 19:41:14 +00:00
|
|
|
}
|
2023-03-21 13:52:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-03-30 21:01:28 +00:00
|
|
|
|
2023-08-03 12:24:23 +00:00
|
|
|
func (o *Manager) processCollectionData(collections []thirdparty.CollectionData) error {
|
2023-08-07 22:30:18 +00:00
|
|
|
return o.collectionsDataDB.SetData(collections)
|
2023-07-31 19:41:14 +00:00
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
func (o *Manager) getCacheFullCollectibleData(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
2023-07-31 19:41:14 +00:00
|
|
|
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
collectiblesData, err := o.collectiblesDataDB.GetData(uniqueIDs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-31 19:41:14 +00:00
|
|
|
|
|
|
|
contractIDs := make([]thirdparty.ContractID, 0, len(uniqueIDs))
|
|
|
|
for _, id := range uniqueIDs {
|
|
|
|
contractIDs = append(contractIDs, id.ContractID)
|
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
collectionsData, err := o.collectionsDataDB.GetData(contractIDs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-31 19:41:14 +00:00
|
|
|
|
|
|
|
for _, id := range uniqueIDs {
|
2023-08-07 22:30:18 +00:00
|
|
|
collectibleData, ok := collectiblesData[id.HashKey()]
|
|
|
|
if !ok {
|
2023-07-31 19:41:14 +00:00
|
|
|
// Use empty data, set only ID
|
2023-08-07 22:30:18 +00:00
|
|
|
collectibleData = thirdparty.CollectibleData{
|
2023-07-31 19:41:14 +00:00
|
|
|
ID: id,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-07 22:30:18 +00:00
|
|
|
collectionData, ok := collectionsData[id.ContractID.HashKey()]
|
|
|
|
if !ok {
|
|
|
|
// Use empty data, set only ID
|
|
|
|
collectionData = thirdparty.CollectionData{
|
|
|
|
ID: id.ContractID,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-31 19:41:14 +00:00
|
|
|
fullData := thirdparty.FullCollectibleData{
|
2023-08-07 22:30:18 +00:00
|
|
|
CollectibleData: collectibleData,
|
|
|
|
CollectionData: &collectionData,
|
2023-07-31 19:41:14 +00:00
|
|
|
}
|
|
|
|
ret = append(ret, fullData)
|
|
|
|
}
|
2023-08-07 22:30:18 +00:00
|
|
|
|
|
|
|
return ret, nil
|
2023-07-18 15:01:53 +00:00
|
|
|
}
|
2023-09-21 12:40:58 +00:00
|
|
|
|
|
|
|
func (o *Manager) FetchCollectibleCommunityInfo(communityID string, id thirdparty.CollectibleUniqueID) (*thirdparty.CollectiblesCommunityInfo, error) {
|
|
|
|
if o.communityInfoProvider == nil {
|
|
|
|
return nil, fmt.Errorf("CollectibleCommunityInfoProvider not available")
|
|
|
|
}
|
|
|
|
|
|
|
|
return o.communityInfoProvider.FetchCollectibleCommunityInfo(communityID, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Manager) FetchCollectibleCommunityTraits(communityID string, id thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleTrait, error) {
|
|
|
|
if o.communityInfoProvider == nil {
|
|
|
|
return nil, fmt.Errorf("CollectibleCommunityInfoProvider not available")
|
|
|
|
}
|
|
|
|
|
|
|
|
traits, err := o.communityInfoProvider.FetchCollectibleCommunityTraits(communityID, id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
collectibleIDs := []thirdparty.CollectibleUniqueID{id}
|
|
|
|
|
|
|
|
collectiblesData, err := o.collectiblesDataDB.GetData(collectibleIDs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if collectible, ok := collectiblesData[id.HashKey()]; ok {
|
|
|
|
collectible.Traits = traits
|
|
|
|
collectiblesData[id.HashKey()] = collectible
|
|
|
|
err = o.collectiblesDataDB.SetData(mapToList(collectiblesData))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return traits, nil
|
|
|
|
}
|
2023-09-22 13:18:42 +00:00
|
|
|
|
|
|
|
// Reset connection status to trigger notifications
|
|
|
|
// on the next status update
|
|
|
|
func (o *Manager) ResetConnectionStatus() {
|
|
|
|
for _, status := range o.statuses {
|
|
|
|
status.ResetStateValue()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Manager) checkConnectionStatus(chainID walletCommon.ChainID) {
|
|
|
|
for _, provider := range o.collectibleProviders {
|
|
|
|
if provider.IsChainSupported(chainID) && provider.IsConnected() {
|
|
|
|
o.statuses[chainID.String()].SetIsConnected(true)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
o.statuses[chainID.String()].SetIsConnected(false)
|
|
|
|
}
|